Skip to content
Back to writing
2 min read

Rate Limiting Without the Headaches

A practical guide to implementing rate limiting that protects your API without punishing legitimate users.

BackendRate LimitingTypeScript

Rate limiting is one of those features everyone knows they need but few implement well. Done wrong, it blocks real users. Done right, it's invisible.

Choose the right algorithm

AlgorithmBest for
Fixed windowSimple limits, low traffic
Sliding windowSmooth limiting, API endpoints
Token bucketBurst tolerance, user-facing features

For most APIs, sliding window hits the sweet spot between accuracy and simplicity.

Key design decisions

Identifier strategy

Rate limit by what matters for your threat model:

  • IP address — anonymous endpoints
  • User ID — authenticated routes
  • API key — third-party integrations

Response headers

Always return standard headers so clients can self-regulate:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1698765432
Retry-After: 60

Graceful degradation

When Redis is down, fail open for read endpoints and fail closed for write endpoints. Document this behavior explicitly.

Middleware pattern

Keep rate limiting as middleware, not scattered across handlers:

app.use("/api/*", rateLimit({
  window: "1m",
  max: 100,
  key: (req) => req.user?.id ?? req.ip,
}));

One place to configure, one place to debug.

Testing rate limits

Don't skip testing the limit itself. Write integration tests that:

  1. Make requests up to the limit
  2. Verify the 429 response
  3. Confirm headers are correct
  4. Wait for reset and verify recovery

Rate limiting done well protects your infrastructure while staying completely invisible to users who play by the rules.