Rate Limiting
DBS includes a lightweight in-memory rate limiter that protects all API routes from excessive requests. It is built into apiGuard and applied automatically to every protected route.
Default Behavior
Every call to apiGuard() automatically enforces rate limiting:
- Limit: 200 requests per 60 seconds
- Key: Per user ID (authenticated users are rate-limited individually)
- Storage: In-memory (
Mapin the Node.js process) - Response on exceed:
429 Too Many Requests
// This already includes rate limiting:
const guard = await apiGuard('user.read');
if (guard.error) return guard.error; // could be 429Implementation
The rate limiter lives in src/lib/rate-limit.ts:
// src/lib/rate-limit.ts
interface RateLimitConfig {
maxRequests: number; // e.g., 200
windowMs: number; // e.g., 60_000 (60 seconds)
}
interface RateLimitEntry {
count: number;
resetAt: number;
}
const store = new Map<string, RateLimitEntry>();
export function checkRateLimit(
key: string,
config: RateLimitConfig = { maxRequests: 200, windowMs: 60_000 }
): { limited: boolean; remaining: number; resetAt: number } {
const now = Date.now();
const entry = store.get(key);
if (!entry || now > entry.resetAt) {
store.set(key, { count: 1, resetAt: now + config.windowMs });
return { limited: false, remaining: config.maxRequests - 1, resetAt: now + config.windowMs };
}
entry.count += 1;
if (entry.count > config.maxRequests) {
return { limited: true, remaining: 0, resetAt: entry.resetAt };
}
return { limited: false, remaining: config.maxRequests - entry.count, resetAt: entry.resetAt };
}Custom Rate Limits Per Route
If a specific route needs tighter or looser limits, apply the rate limiter directly before or after apiGuard:
Stricter (Auth Routes)
// src/app/api/auth/send-otp/route.ts
import { checkRateLimit } from '@/lib/rate-limit';
import { apiGuard } from '@/lib/api-guard';
export async function POST(req: Request) {
const guard = await apiGuard();
if (guard.error) return guard.error;
// Maximum 5 OTP emails per 10 minutes
const { limited } = checkRateLimit(`otp:${guard.session.user.id}`, {
maxRequests: 5,
windowMs: 10 * 60 * 1000,
});
if (limited) {
return Response.json(
{ error: 'Too many OTP requests. Please wait before trying again.' },
{ status: 429 }
);
}
// send OTP...
}Error Response
When rate limiting triggers, the API returns:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
"error": "Rate limit exceeded, please try again later."
}The frontend’s error handling in useData and the service layer surfaces this as a toast notification.
The in-memory rate limiter is per-instance. In a horizontally scaled environment (multiple Node.js processes or containers), each instance maintains its own count. For strict rate limiting across multiple instances, consider using a Redis-backed rate limiter (e.g., @upstash/ratelimit). Only the implementation in rate-limit.ts needs to change — apiGuard remains the same.
Rate Limit Headers
Consider adding standard headers to rate-limited responses for better client integration:
return new Response(JSON.stringify({ error: 'Rate limit exceeded' }), {
status: 429,
headers: {
'Content-Type': 'application/json',
'X-RateLimit-Limit': String(config.maxRequests),
'X-RateLimit-Remaining': String(remaining),
'X-RateLimit-Reset': String(Math.floor(resetAt / 1000)),
'Retry-After': String(Math.ceil((resetAt - Date.now()) / 1000)),
},
});