← Recipes

阅读中文版本

Implement a Sliding-Window Rate Limit

Use a sliding window or token bucket with shared state so limits are accurate across instances and fair at the boundaries of fixed windows.

~40 minrate-limitingreliabilitysecurity

Task

Add a rate limiter that is accurate across multiple servers and avoids the burst at fixed-window boundaries.

When to use this

A per-instance fixed-window limiter lets a caller send double the allowed rate at the window boundary, and it multiplies the limit by the number of replicas.

Prerequisites

  • A shared store for the counters
  • A key per client identity

Steps

1. Choose sliding-window or token-bucket instead of a fixed window.

Expected output: The limit is smooth rather than bursty at boundaries.

2. Store counters or tokens in a shared store keyed by client and route.

Expected output: The limit applies globally, not per instance.

3. Update and read counters atomically with a Lua script or an atomic operation.

Expected output: Concurrent requests cannot exceed the limit.

4. Return 429 with Retry-After and limit headers so clients back off correctly.

redis-cli --eval ratelimit.lua client:42 , 100 60

Expected output: Well-behaved clients adjust automatically.

Troubleshooting

Problem

Counters lost on restart

Solution

Keep the counter in a shared durable store, not in process memory.

Problem

Limit applied to the wrong key

Solution

Key by the correct identity (api key or user), not by the ephemeral connection address.

Limitations

  • A shared-store limiter adds a round trip per request and can itself become a bottleneck.
  • Distributed rate limiting is approximate under partitions and needs a documented failure mode.

Context

Add a rate limiter that is accurate across multiple servers and avoids the burst at fixed-window boundaries. A per-instance fixed-window limiter lets a caller send double the allowed rate at the window boundary, and it multiplies the limit by the number of replicas.

Steps

  1. Choose sliding-window or token-bucket instead of a fixed window. Expected: The limit is smooth rather than bursty at boundaries.
  2. Store counters or tokens in a shared store keyed by client and route. Expected: The limit applies globally, not per instance.
  3. Update and read counters atomically with a Lua script or an atomic operation. Expected: Concurrent requests cannot exceed the limit.
  4. Return 429 with Retry-After and limit headers so clients back off correctly. Expected: Well-behaved clients adjust automatically.
const allowed = await redis.eval(TOKEN_BUCKET_LUA, 1, `rl:${clientId}`, now, rate, capacity);
if (!allowed) {
  res.setHeader('Retry-After', '1');
  return res.status(429).json({ error: { code: 'rate_limited' } });
}

Verify

  • The limit is smooth rather than bursty at boundaries.
  • The limit applies globally, not per instance.
  • Concurrent requests cannot exceed the limit.
  • Well-behaved clients adjust automatically.

Limits

  • A shared-store limiter adds a round trip per request and can itself become a bottleneck.
  • Distributed rate limiting is approximate under partitions and needs a documented failure mode.