API Rate Limiting: Token Bucket, 429, Retry-After
API Rate Limiting: Token Bucket, 429, Retry-After
Rate limiting caps how many requests a single client may fire against your API документацию. Goals: prevent brute force on auth endpoints, absorb DDoS, protect infrastructure from abusive heavy queries, and fairly partition capacity across pricing tiers. This post covers the algorithms (token bucket, sliding window), HTTP 429 + Retry-After, and ready-to-ship snippets for Redis, nginx and Express.
Why rate limit
- Defend against credential stuffing
- Resist scraping and data exfiltration
- Cap cost: paid APIs (Maps, OpenAI) will blow your budget without limits
- Business tiering: free=100/day, pro=10,000/day
- Fair use: no single client can DoS your service for everyone else
Algorithms
Fixed Window
Counter resets every minute/hour. Pros: trivial. Cons: "boundary burst" — 100 requests at 23:59 plus 100 more at 00:00 = 200 in 2 seconds.
Sliding Window
Accounts for a rolling N-second window. Accurate, more memory. Implement via Redis sorted sets of timestamps:
ZADD ratelimit:user:123 {now} {now}
ZREMRANGEBYSCORE ratelimit:user:123 0 {now - 60}
ZCARD ratelimit:user:123 ← current count in the last 60s
EXPIRE ratelimit:user:123 120
Token Bucket
The bucket holds N tokens, refills at M/sec. Each request removes one token; empty = 429. Great for bursty traffic (cache warm-up, массовую проверку URL ops).
Leaky Bucket
Inverse of token bucket: requests drain at a fixed rate. Smooth, but no bursting allowed.
HTTP 429 and Retry-After
On limit exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705324800
Content-Type: application/json
{"error":"rate_limit_exceeded","message":"Try again in 30 seconds"}
Retry-After — seconds or HTTP date. curl and most clients honour it with backoff.
Redis implementation (PHP)
function checkRateLimit(Redis $r, string $key, int $max, int $window): bool {
$now = microtime(true);
$r->zAdd("rl:{$key}", $now, "{$now}");
$r->zRemRangeByScore("rl:{$key}", 0, $now - $window);
$count = $r->zCard("rl:{$key}");
$r->expire("rl:{$key}", $window * 2);
return $count <= $max;
}
if (!checkRateLimit($redis, "user:{$userId}", 60, 60)) {
header('HTTP/1.1 429 Too Many Requests');
header('Retry-After: 60');
exit(json_encode(['error' => 'rate_limit_exceeded']));
}
nginx
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
}
location /api/auth/login {
limit_req zone=login burst=3 nodelay;
proxy_pass http://backend;
}
}
Express / NestJS
// Express
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
app.use('/api/', rateLimit({
store: new RedisStore({ client: redisClient }),
windowMs: 60_000,
max: 60,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: (req) => req.user?.id || req.ip,
}));
// NestJS
ThrottlerModule.forRoot({
throttlers: [{ ttl: 60_000, limit: 60 }],
storage: new RedisThrottlerStorage(redis),
})
Keying strategy: IP vs user vs API key
- IP — catches anonymous abuse but NAT produces false positives in office networks
- User ID — accurate for authenticated traffic
- API key — for public APIs, supports tiered plans
Best practice is a composite key: user_id || api_key || ip.
Auth endpoints: special rules
Login/register/password-reset: cap at 5 attempts per 15 minutes per IP and per email. Pair with a slow bcrypt (cost ≥12) and CAPTCHA after 3 failures.
Monitoring
Watch metrics: percent of 429s, top-10 throttled IPs, latency under throttle. Set up enterno monitoring to alert on a rising 429 rate — it signals a DDoS or a buggy client.
FAQ
429 vs 503? 429 — this client exceeded its quota. 503 — server is globally overloaded.
Do I need Retry-After? Strongly recommended — without it clients busy-loop and make the outage worse.
How to protect against distributed DDoS? App-level rate limits are not enough — use Cloudflare/AWS Shield or a WAF.
Can rate limits be bypassed? IP rotation via proxies, API-key rotation, free-trial abuse. Mitigate with fingerprinting and behavioural analysis.
Conclusion
Minimum: Redis sliding window, 429 with Retry-After, and a stricter rule for auth endpoints. For serious adversaries add a WAF. Track API health via enterno monitors. Related: CORS, HTTP Security Headers.
Check your website right now
Check now →