Claude Fable 5.1 & GPT-6 Astra packages are live

Rate Limiting

Free · MIT

Limiting request rates without breaking legitimate users — algorithm choice, correct keys, distributed state, and the headers clients need.

218 lines8.0 KB Claude Security
Target models
Claude Fable 5.1Claude Opus 5Claude Sonnet 5Claude 5 FamilyFuture Claude Models
Name
rate-limiting
Category
Security
Description
Limiting request rates without breaking legitimate users — algorithm choice, correct keys, distributed state, and the headers clients need.
License
MIT
Author
Agent.md maintainers
Last verified
2026-08-23
Reviewed by
unreviewed

FORBIDDEN: Truncating code or writing placeholders such as "// ... existing code ..." or "# rest unchanged". Every edit is complete and applies as written. FORBIDDEN: Reporting a check as passed without showing the command and its output. REQUIRED: Reason through the rules below before the first edit; when two rules conflict, the one stated first wins.

  • Never trust X-Forwarded-For blindly. It is client-settable; an attacker prepends a fake address and evades every per-IP limit. Configure trust proxy to the exact number of proxies you run and take the correct position from the right.
  • Never key on User-Agent, a cookie the client controls, or a request body field. All are attacker-chosen.

#Purpose

Rules for throttling requests to protect capacity and to slow abuse. Rate limiting is a control on volume, not a substitute for authentication or authorisation.

Get the key right before the algorithm. Limiting the wrong dimension is the most common failure: per-IP alone does not stop distributed credential stuffing against one account, and per-account alone lets one host spray many accounts.


#Choosing the key

KeyProtects againstWeakness
API key or user idPer-tenant fairness, quotaRequires authentication first
Account identifierCredential stuffing on one accountAttacker rotates accounts
Client IPSingle noisy sourceNAT and CGNAT share IPs; IPv6 rotates cheaply
IP + accountLogin endpoints
Endpoint cost classExpensive operationsNeeds per-route configuration

For login, limit per account and per IP independently — a request is rejected if either budget is exhausted.

Never trust X-Forwarded-For blindly. It is client-settable; an attacker prepends a fake address and evades every per-IP limit. Configure trust proxy to the exact number of proxies you run and take the correct position from the right.

js
app.set("trust proxy", 1);      // exactly one proxy in front — not `true`

Never key on User-Agent, a cookie the client controls, or a request body field. All are attacker-chosen.


#Algorithms

AlgorithmBehaviourUse
Token bucketSteady refill, allows bursts up to capacityDefault — matches real traffic
Sliding window logExact; stores each timestampLow volume, strict accuracy
Sliding window counterApproximate, cheapHigh volume
Fixed windowSimple counter per intervalAvoid — see below
Leaky bucketSmooths output to a constant rateQueue-shaped workloads

Avoid fixed windows. They permit a double burst at the boundary: a client spends the whole budget at 00:59 and the whole next budget at 01:01, sending two allowances within two seconds.

js
// Token bucket in Redis — atomic, so concurrent requests cannot both pass.
const ALLOW = `
local key, rate, burst, now = KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
local b = redis.call('HMGET', key, 'tokens', 'ts')
local tokens = tonumber(b[1]) or burst
local ts = tonumber(b[2]) or now
tokens = math.min(burst, tokens + (now - ts) * rate)
if tokens < 1 then
  redis.call('HMSET', key, 'tokens', tokens, 'ts', now)
  return 0
end
redis.call('HMSET', key, 'tokens', tokens - 1, 'ts', now)
redis.call('EXPIRE', key, math.ceil(burst / rate) * 2)
return 1
`;
const allowed = await redis.eval(ALLOW, 1, `rl:${key}`, rate, burst, Date.now() / 1000);

The Lua script matters: read-then-write from application code is a race, and under concurrency more requests pass than the limit permits.


#Distributed state

  • An in-memory counter per process means the real limit is limit × instances, and it resets on every deploy. Acceptable for a single instance; wrong for anything scaled.
  • Use a shared store — Redis, or the platform's own limiter at the edge.
  • Prefer limiting at the edge (CDN, API gateway) for volumetric abuse: it never reaches your origin. Keep application-level limits for per-account and per-endpoint rules the edge cannot see.
  • Fail open or closed deliberately. If Redis is down, decide in advance whether to allow (availability) or deny (protection), and log the decision. Silently allowing because an exception was swallowed is the common accident.

#Responding

makefile
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
Retry-After: 30
  • Return 429, not 403. 403 tells a client it is forbidden forever.
  • Always send Retry-After. Without it, well-behaved clients retry immediately and make the situation worse.
  • Expose remaining budget so clients can self-pace.
  • Apply jitter to any server-suggested backoff, or every throttled client returns simultaneously.
  • Never leak whether an account exists through differing limits — see Security/authentication.

#Tuning

  • Measure real traffic before setting a limit. A limit below the p99 of legitimate use is an outage you scheduled for yourself.
  • Set different limits per endpoint class: a search or export endpoint costs orders of magnitude more than a health check.
  • Run in observe-only first, logging what would have been rejected.
  • Exempt health checks and internal service traffic explicitly, by credential — never by IP range alone.
  • Alert on sustained 429 rates. A spike is either an attack or a broken client, and both are worth knowing about.

#Anti-patterns

Anti-patternWhy it failsFix
Trusting X-Forwarded-ForClient-settable; trivially spoofedConfigure trust proxy precisely
In-memory counters across instancesReal limit is limit × instancesShared store
Read-then-write without atomicityConcurrent requests overshootLua script or atomic primitive
Fixed windowDouble burst at the boundaryToken bucket
Per-IP only on loginDistributed stuffing passesAlso limit per account
403 instead of 429Reads as permanent denial429 with Retry-After
No Retry-AfterClients retry immediatelyAlways send it
Fail-open by swallowed exceptionLimit silently disappearsDecide and log the mode
Limit set without measuringBlocks legitimate usersObserve first
Permanent lockout on failuresSelf-inflicted denial of serviceExponential backoff

#Checklist

  • Limits are keyed on account and IP independently for authentication routes
  • trust proxy is set to the exact proxy count; X-Forwarded-For is not trusted raw
  • Algorithm is token bucket or sliding window, never fixed window
  • Counter updates are atomic under concurrency
  • State is shared across instances, not per-process memory
  • Store-unavailable behaviour is a deliberate, logged decision
  • Responses return 429 with Retry-After and remaining budget
  • Backoff guidance includes jitter
  • Limits differ by endpoint cost class
  • Limits were derived from measured traffic and trialled in observe-only mode
  • Health checks and internal traffic are exempted by credential
  • Sustained 429 rates raise an alert