Skip to content
← All articles

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

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

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 →
More articles: SEC
SEC
Cookie Security: HttpOnly, Secure, SameSite, __Host-
15.04.2026 · 7 views
SEC
HTTP to HTTPS Migration: Redirects, Mixed Content, HSTS
15.04.2026 · 5 views
SEC
Subresource Integrity (SRI): Protecting CDN Scripts
15.04.2026 · 5 views
SEC
Clickjacking Prevention: X-Frame-Options vs frame-ancestors
15.04.2026 · 6 views