Fleet-wide Auth Rate Limiting
Fleet-wide Auth Rate Limiting
Introduced in v1.0.45 (SEC-14)
Calmony Pay enforces rate limits on all authentication and sensitive API endpoints to prevent brute-force and credential-stuffing attacks. From v1.0.45, these limits are enforced fleet-wide across all Vercel edge instances when Upstash Redis is configured.
The Problem with Per-instance Limits
On Vercel, each edge or serverless function instance has isolated memory. A synchronous, in-memory rate limiter means each instance maintains its own independent counter. A determined attacker distributing requests across instances can exceed the intended limit by a factor equal to the number of active instances.
How Fleet-wide Limiting Works
The checkRateLimitEdge function (used by the Next.js middleware) resolves this by using a shared counter in Upstash Redis:
- On each incoming request, middleware extracts the client IP and constructs a key in the format
<ip>:<route-label>. - A single pipelined HTTP request is sent to the Upstash Redis REST API, executing
INCRandPEXPIRE NXatomically. - The returned counter is compared against the configured limit. If the counter exceeds the limit, a
429 Too Many Requestsresponse is returned immediately. - The Redis key is namespaced by fixed-window epoch (
rl:<key>:<windowEpoch>) so keys are naturally self-expiring — no cleanup job is required.
The pipeline adds approximately 10–20 ms of latency per rate-limited request, which is negligible for auth-endpoint traffic.
Fallback Behaviour
The implementation is designed to never block legitimate traffic due to infrastructure issues:
- No Redis configured → falls back to per-instance in-memory sliding window (zero latency, useful in development and as defence-in-depth).
- Redis unreachable → fails open (request is allowed through) and emits a
console.warnso operators can investigate misconfiguration.
Configuration
Set the following environment variables to enable fleet-wide enforcement:
UPSTASH_REDIS_REST_URL=https://<your-instance>.upstash.io
UPSTASH_REDIS_REST_TOKEN=<your-token>
Obtain these from the Upstash Console after creating a Redis database. Both variables must be present; if either is missing the system silently falls back to in-memory limiting.
Development: You do not need an Upstash instance to run locally. The in-memory fallback is active automatically when the env vars are absent.
Protected Endpoints
The following endpoints are guarded by fleet-wide rate limiting:
| Endpoint | Methods | Limit | Purpose |
|---|---|---|---|
/api/auth/* | POST, GET | 10 req/min/IP | NextAuth sign-in and OAuth |
/sign-in | POST | 10 req/min/IP | Password sign-in |
/sign-up | POST | 10 req/min/IP | New account registration |
/invite/* | POST, GET | 20 req/min/IP | Invite token enumeration prevention |
/api/trpc/invite | POST | 30 req/min/IP | Invite management tRPC calls |
/api/trpc/org | POST | 30 req/min/IP | Organisation management tRPC calls |
When a limit is exceeded the middleware returns:
HTTP/1.1 429 Too Many Requests
Retry-After: <seconds until window reset>
A security log event is also emitted for breaches on auth and invite routes.
API Reference
checkRateLimitEdge(key, limit, windowMs): Promise<RateLimitResult>
Async rate-limit check. Uses Upstash Redis when configured, falls back to in-memory.
| Parameter | Type | Description |
|---|---|---|
key | string | Unique identifier for this client+route, e.g. <ip>:auth |
limit | number | Maximum requests allowed per windowMs |
windowMs | number | Window duration in milliseconds |
Returns a RateLimitResult:
interface RateLimitResult {
allowed: boolean; // Whether the request should proceed
limit: number; // Total requests allowed per window
remaining: number; // Requests remaining in the current window
reset: number; // Epoch-ms when the window resets
}
checkRateLimit(key, limit, windowMs): RateLimitResult
Synchronous in-memory sliding-window check. Retained for backwards compatibility with callers that cannot await. Does not use Redis.