Rate Limiting
In-memory sliding window rate limiter protecting all API routes with tiered limits.
Overview
All API routes are rate-limited to prevent brute-force attacks, abuse, and denial-of-service. The limiter runs as middleware, checking every request before it reaches the route handler.
Algorithm
The rate limiter uses a sliding window algorithm. Instead of resetting counters at fixed intervals (which allows burst traffic at window boundaries), it tracks individual request timestamps and counts only those within the current window.
Tiers
Different route groups have different limits based on their sensitivity and expected usage patterns:
| Tier | Routes | Window | Max Requests |
|---|---|---|---|
auth | /api/auth/* | 60 seconds | 10 |
dashboard | /api/dashboard/* | 60 seconds | 60 |
api | /api/v1/* | 60 seconds | 120 |
execute | /api/v1/tools/execute | 60 seconds | 30 |
The auth tier is the strictest because authentication endpoints are the primary target for brute-force attacks. The execute tier is limited because each tool execution triggers an outbound HTTP request.
Client Identification
The limiter identifies clients using a composite key:
- API key prefix — for SDK requests that include an API key, the first 16 characters of the key are used
- IP address — for all other requests, the client IP is extracted from
x-forwarded-for(first entry) orx-real-ipheaders
This means:
- Different API keys from the same IP get separate rate limit buckets
- Requests without an API key share a bucket per IP
- A single API key is rate-limited consistently regardless of which IP it comes from
Response Headers
When rate-limited, the response includes:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
"error": "Rate limit exceeded. Try again in 45s"
}Implementation
The limiter is implemented in lib/security/rate-limit.ts and called from src/middleware.ts. It uses an in-memory Map to store request timestamps per key.
Stale entries are cleaned up on every check — timestamps outside the current window are removed automatically.
Memory Considerations
The in-memory store works well for single-instance deployments. For multi-instance or serverless deployments (e.g. Vercel Functions), each instance maintains its own counters. This means:
- Effective limits are higher than configured (multiplied by instance count)
- Cold starts reset counters
For production deployments that need strict global limits, replace the in-memory store with Redis using the same checkRateLimit interface.
Customization
To adjust limits for your deployment, modify the RATE_LIMIT_TIERS object in lib/security/rate-limit.ts:
export const RATE_LIMIT_TIERS = {
auth: { windowMs: 60_000, maxRequests: 10 },
dashboard: { windowMs: 60_000, maxRequests: 60 },
api: { windowMs: 60_000, maxRequests: 120 },
execute: { windowMs: 60_000, maxRequests: 30 },
};