All Docs
FeaturesCalmony PayUpdated March 15, 2026

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:

  1. On each incoming request, middleware extracts the client IP and constructs a key in the format <ip>:<route-label>.
  2. A single pipelined HTTP request is sent to the Upstash Redis REST API, executing INCR and PEXPIRE NX atomically.
  3. The returned counter is compared against the configured limit. If the counter exceeds the limit, a 429 Too Many Requests response is returned immediately.
  4. 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.warn so 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:

EndpointMethodsLimitPurpose
/api/auth/*POST, GET10 req/min/IPNextAuth sign-in and OAuth
/sign-inPOST10 req/min/IPPassword sign-in
/sign-upPOST10 req/min/IPNew account registration
/invite/*POST, GET20 req/min/IPInvite token enumeration prevention
/api/trpc/invitePOST30 req/min/IPInvite management tRPC calls
/api/trpc/orgPOST30 req/min/IPOrganisation 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.

ParameterTypeDescription
keystringUnique identifier for this client+route, e.g. <ip>:auth
limitnumberMaximum requests allowed per windowMs
windowMsnumberWindow 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.