Rate Limit
Throttle by IP, user, route or endpoint
Rate limiting protects your API from abuse, brute-force and accidental hammering. Nucleus ships a Redis-backed limiter with three algorithms and independent budgets for public, private and — most importantly — sensitive auth routes.
Limits are keyed by IP and/or endpoint (per-user keying exists in the RateLimiter service but is NOT wired into the default request pipeline — the middleware runs before auth), so shipped rules look like “5 logins per 15 minutes per IP”. Standard rate-limit headers tell clients exactly where they stand.
Strategy#
Choose the algorithm and the Redis namespace. The sliding window is the most accurate and the sensible default for most APIs.
enabledbooleanOptionalTurn the limiter on. Defaults to on whenever Redis is configured (the limiter is only instantiated when Redis is present); set false to disable throttling entirely.
truestrategy'sliding-window' | 'fixed-window' | 'token-bucket'OptionalThe counting algorithm. sliding-window smooths bursts across the window boundary; fixed-window is cheapest but allows edge bursts; token-bucket permits controlled bursting up to a refill rate.
sliding-window— Accurate, burst-resistant. Recommended.fixed-window— Cheapest; can allow 2× bursts at window edges.token-bucket— Allows bursts up to a bucket size, refilled over time.
'sliding-window'keyPrefixstringOptionalRedis key prefix for limiter counters — isolate multiple apps on one Redis.
'rl:'failClosedbooleanOptionalBehaviour when the Redis backend errors (not a missing key). true = deny (fail closed) for all categories; false = allow (fail open) for all. Default (unset): fail closed for auth routes only — so a Redis outage can't open a brute-force window on login/register/reset/magic-link — and fail open elsewhere.
Keying dimensions#
What a limit is counted against. Combine these to build precise rules — per-IP stops a single attacker, per-user stops a single account, per-endpoint isolates hot routes.
byIpbooleanOptionalCount requests per client IP.
truebyUserIdbooleanOptionalIntended to count requests per authenticated user, but this dimension is NOT active in the default request pipeline — the built-in middleware runs before authentication and passes no userId, so shipped limiting is effectively per-IP / per-endpoint. Only meaningful for direct RateLimiter.check() calls you make yourself with a userId.
true (but not wired)byEndpointbooleanOptionalTrack each endpoint separately so a busy route doesn't exhaust another's budget.
falseskipSuccessfulRequestsbooleanOptionalA RateLimiter-service capability (implemented via decrement()) that is NOT applied by the default request middleware — enabling it has no effect on shipped behaviour unless you call the service yourself.
false (not applied)Budgets#
Per-category limits. Each budget is a window (duration string) plus a max count. Defaults: login 5 per 15m (+30m lockout), register 3 per 1h (+1h lockout), passwordReset 3 per 1h (+1h lockout), magicLink 5 per 1h (+1h lockout), the generic authRoutes fallback 10 per 1m, publicRoutes 100 per 1m, privateRoutes 60 per 1m. Auth routes are classified from your resolved config (a customized login.route is honoured, matched on a path boundary); /auth/invite and the session approve/reject endpoints (the second half of a new-device login challenge) are folded into the strict auth category too — so a customized or invite route can't silently fall back to the lax private limit.
1{2 "rateLimit": {3 "enabled": true,4 "strategy": "sliding-window",5 "byIp": true,6 "authRoutes": {7 "login": { "window": "1m", "max": 5, "blockDuration": "15m" },8 "register": { "window": "1h", "max": 10 }9 },10 "privateRoutes": { "window": "1m", "max": 120 }11 }12}authRoutesobjectOptionalBudgets for authentication endpoints: a shared default plus stricter per-route overrides. Each override is { window, max } and the sensitive ones add blockDuration — a cooldown that locks the client out after the limit trips.
windowstringOptionalDefault window applied to all auth routes, e.g. "1m".
maxnumberOptionalDefault max requests per window across auth routes.
login{ window?; max?; blockDuration? }OptionalOverride for POST /auth/login. blockDuration locks the client out after the limit trips, e.g. "15m".
register{ window?; max?; blockDuration? }OptionalOverride for POST /auth/register.
passwordReset{ window?; max?; blockDuration? }OptionalOverride for the password-reset request/confirm routes.
magicLink{ window?; max?; blockDuration? }OptionalOverride for magic-link issuance.
publicRoutes{ window?: string; max?: number }OptionalBudget applied to unauthenticated/public endpoints.
privateRoutes{ window?: string; max?: number }OptionalBudget applied to authenticated endpoints.
Response headers & allow/deny lists#
Control the feedback clients receive and carve out exceptions. Whitelisted clients bypass limits; blacklisted ones are always rejected.
headers{ remaining?; reset?; limit? }OptionalCustom names for the rate-limit response headers (remaining quota, reset time, total limit) so clients can back off gracefully. Defaults: remaining 'X-RateLimit-Remaining', reset 'X-RateLimit-Reset' (emitted as a Unix epoch-seconds timestamp), limit 'X-RateLimit-Limit'.
whiteliststring[]OptionalIdentifiers (e.g. IPs) that bypass rate limiting entirely.
blackliststring[]OptionalIdentifiers that are always blocked, regardless of budget. Entries are exact strings or * glob wildcards (not CIDR); blacklisted IPs are refused with a fixed 24h Retry-After.
trustedProxiesstring[]OptionalIPs/CIDRs of trusted reverse proxies or load balancers. The client IP is taken from the TCP socket peer, and X-Forwarded-For (falling back to X-Real-IP) is honored only when the peer matches a trustedProxies entry, walking the chain right-to-left past trusted hops. Behind a proxy you MUST set this, or every client collapses into the proxy's single bucket.
Under the hood — the limiter#
Every counter lives in Redis (keyed by prefix + category + auth-type + the enabled ip/user/endpoint dimensions), so limits hold across instances. The three strategies differ only in how they count.
sliding-windowtimestamp listOptionalKeeps the timestamps of recent hits and drops any older than the window on each check — a precise rolling count with no bucket-boundary bursts. The default strategy.
fixed-windowper-bucket counterOptionalIncrements a counter keyed by the current window id (now / windowMs). Cheapest, but allows up to 2× max across a boundary.
token-bucketrefill rateOptionalRefills tokens at max/window and spends one per request, allowing controlled bursts up to the bucket size while smoothing the sustained rate.
lockout (blockDuration):blocked keyOptionalWhen an auth route with a blockDuration trips its limit, a {key}:blocked marker is written with an until timestamp; subsequent requests are refused with a retryAfter until it expires — so brute-forcing login costs the attacker a real lockout, not just a slow drip. Caveat: blockDuration is honored only by the sliding-window and fixed-window strategies; the token-bucket strategy never writes a :blocked marker, so switching strategy to 'token-bucket' disables auth lockout.
skipSuccessfulRequestsrefundOptionalWhen enabled, a request that ultimately succeeds is refunded (a timestamp popped / the counter decremented), so only failed attempts erode the budget — ideal for login where you want to punish wrong passwords, not correct ones.
allow/deny matchingglobOptionalwhitelist and blacklist entries support * wildcards (compiled to a regex). Whitelisted IPs bypass entirely; blacklisted IPs are refused for 24h before any counting happens.
Related sections