---
# === IDENTITY ===
id: software/patterns/api-rate-limiting/2026
canonical_question: "How do I implement API rate limiting?"
aliases:
  - "API rate limiter"
  - "throttling API requests"
  - "token bucket rate limiting"
  - "sliding window rate limiter"
  - "request rate limiting middleware"
  - "distributed rate limiting"
  - "how to rate limit an API"
  - "429 Too Many Requests handling"
entity_type: software_reference
domain: software > patterns > api_rate_limiting
region: global
jurisdiction: global
temporal_scope: 2015-2026

# === VERIFICATION ===
last_verified: 2026-02-24
confidence: 0.92
version: 1.0
first_published: 2026-02-24

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "IETF RateLimit header fields draft (draft-ietf-httpapi-ratelimit-headers) approaching RFC status (2024); GCRA gaining adoption as preferred algorithm in cloud-native systems (2023-2025)"
  next_review: 2026-08-23
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "Rate limiting checks MUST happen before expensive operations (authentication, database queries, business logic) — never after"
  - "Distributed rate limiting requires atomic read-increment operations — naive get-then-set causes race conditions that allow burst overflows"
  - "Always return 429 status code (RFC 6585) with Retry-After header — omitting Retry-After causes aggressive client retries that amplify load"
  - "In-memory rate limiters only work for single-instance deployments — multi-instance requires shared storage (Redis, Memcached, or database)"
  - "Rate limit keys must be chosen carefully — per-IP fails behind NAT/proxies (thousands of users share one IP), per-API-key fails for unauthenticated endpoints"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need to design the overall system architecture for a rate limiter service"
    use_instead: "software/system-design/rate-limiter/2026"
  - condition: "Need to handle 429 errors as an API client (not server)"
    use_instead: "software/patterns/retry-exponential-backoff/2026"
  - condition: "Need DDoS protection at the network layer"
    use_instead: "Use CDN/WAF-level rate limiting (Cloudflare, AWS WAF, Akamai) — application-level rate limiting is too late for volumetric attacks"

# === AGENT HINTS ===
inputs_needed:
  - key: "algorithm"
    question: "Which rate limiting algorithm do you want to use?"
    type: choice
    options: ["token bucket", "sliding window log", "sliding window counter", "fixed window", "leaky bucket", "GCRA"]
  - key: "scope"
    question: "What should the rate limit key be based on?"
    type: choice
    options: ["per-user", "per-IP", "per-API-key", "global"]
  - key: "storage"
    question: "What storage backend are you using for rate limit state?"
    type: choice
    options: ["Redis", "in-memory", "Memcached", "database"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/patterns/api-rate-limiting/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-24)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/patterns/rest-pagination/2026"
      label: "REST API Pagination"
    - id: "software/patterns/api-versioning/2026"
      label: "API Versioning Strategies"
    - id: "software/patterns/api-key-management/2026"
      label: "API Key Management"
  depends_on:
    - id: "software/debugging/redis-memory-issues/2026"
      label: "Redis Memory Issues"
  solves:
    - id: "software/patterns/circuit-breaker/2026"
      label: "Circuit Breaker Pattern"
    - id: "software/patterns/retry-exponential-backoff/2026"
      label: "Retry with Exponential Backoff"
  often_confused_with:
    - id: "software/system-design/rate-limiter/2026"
      label: "Rate Limiter System Design — full distributed architecture, not implementation pattern"

# === SOURCES (8 authoritative sources) ===
sources:
  - id: src1
    title: "Rate Limiting Guide: Algorithms & Best Practices"
    author: API7.ai
    url: https://api7.ai/blog/rate-limiting-guide-algorithms-best-practices
    type: technical_blog
    published: 2025-01-15
    reliability: moderate_high
  - id: src2
    title: "Build 5 Rate Limiters with Redis: Comparing Algorithms"
    author: Redis
    url: https://redis.io/learn/howtos/ratelimiting
    type: official_docs
    published: 2024-06-01
    reliability: high
  - id: src3
    title: "RFC 6585 — Additional HTTP Status Codes (429 Too Many Requests)"
    author: IETF
    url: https://datatracker.ietf.org/doc/html/rfc6585
    type: rfc_spec
    published: 2012-04-01
    reliability: authoritative
  - id: src4
    title: "Rate Limiting, Cells, and GCRA"
    author: Brandur Leach
    url: https://brandur.org/rate-limiting
    type: technical_blog
    published: 2017-01-15
    reliability: high
  - id: src5
    title: "How to Build a Distributed Rate Limiting System Using Redis and Lua Scripts"
    author: freeCodeCamp
    url: https://www.freecodecamp.org/news/build-rate-limiting-system-using-redis-and-lua/
    type: technical_blog
    published: 2025-03-10
    reliability: moderate_high
  - id: src6
    title: "Rate Limiting Best Practices in REST API Design"
    author: Speakeasy
    url: https://www.speakeasy.com/api-design/rate-limiting
    type: technical_blog
    published: 2024-09-20
    reliability: moderate_high
  - id: src7
    title: "Rate Limiting Fundamentals"
    author: Alex Xu (ByteByteGo)
    url: https://blog.bytebytego.com/p/rate-limiting-fundamentals
    type: technical_blog
    published: 2024-01-15
    reliability: high
  - id: src8
    title: "API Rate Limiting at Scale: Patterns, Failures, and Control Strategies"
    author: Gravitee
    url: https://www.gravitee.io/blog/rate-limiting-apis-scale-patterns-strategies
    type: technical_blog
    published: 2025-05-01
    reliability: moderate_high
---

# API Rate Limiting: Algorithms, Implementation & Best Practices

## TL;DR

- **Bottom line**: Use token bucket for most APIs (best burst handling + simplicity); use sliding window log when you need precise per-second accuracy; use GCRA for memory-efficient production systems.
- **Key tool/command**: `Redis EVAL` with Lua scripts for atomic, distributed rate limiting
- **Watch out for**: Race conditions in distributed environments when using non-atomic get-then-set operations — always use Lua scripts or Redis transactions.
- **Works with**: Any HTTP API framework (Express, FastAPI, Gin, Spring Boot), any language. Redis 6+ recommended for distributed setups.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Rate limiting checks MUST execute before expensive operations (auth, DB queries, business logic) — never after
- Distributed deployments require atomic read-increment (Lua scripts or MULTI/EXEC) — naive get-then-set allows race condition bursts
- Always return HTTP 429 with `Retry-After` header (RFC 6585) — omitting it causes retry storms [src3]
- In-memory rate limiters only work for single-instance deployments — multi-instance requires shared state (Redis, Memcached)
- Per-IP rate limiting fails behind NAT/proxies where thousands of users share one IP — combine with API key or user ID when possible

## Quick Reference

| Algorithm | Burst Handling | Memory per Key | Accuracy | Distributed Friendly | Complexity | Best For |
|---|---|---|---|---|---|---|
| Token Bucket | Excellent — allows bursts up to bucket capacity | O(1) — 2 values (tokens, timestamp) | Good — approximate over short windows | Yes (with atomic ops) | Low | General API rate limiting |
| Leaky Bucket | None — enforces constant rate | O(1) — 2 values (queue size, timestamp) | Excellent — smooth output | Yes (with atomic ops) | Low | Traffic shaping, constant throughput |
| Fixed Window | Poor — 2x burst at window boundary | O(1) — 1 counter + TTL | Low — boundary spike problem | Yes (INCR + EXPIRE) | Very Low | Simple counters, non-critical limits |
| Sliding Window Log | None — strict enforcement | O(n) — stores every timestamp | Exact — no boundary issues | Moderate (sorted sets) | High | Billing, compliance, audit trails |
| Sliding Window Counter | Good — weighted average smooths boundary | O(1) — 2 counters | Good — approximation within 0.003% | Yes (two counters) | Low | Production APIs (accuracy + efficiency) |
| GCRA | Good — configurable burst via tau | O(1) — 1 timestamp (TAT) | Excellent — continuous enforcement | Yes (single atomic CAS) | Moderate | High-scale production, Stripe/Shopify-style |

### HTTP Response Headers (RFC 6585 + IETF Draft)

| Header | Purpose | Example | Required |
|---|---|---|---|
| `Retry-After` | Seconds until client should retry | `Retry-After: 30` | Yes (with 429) |
| `X-RateLimit-Limit` | Max requests in window | `X-RateLimit-Limit: 100` | Recommended |
| `X-RateLimit-Remaining` | Requests left in current window | `X-RateLimit-Remaining: 42` | Recommended |
| `X-RateLimit-Reset` | Unix timestamp when window resets | `X-RateLimit-Reset: 1708790400` | Recommended |
| `RateLimit-Policy` | Machine-readable policy (IETF draft) | `RateLimit-Policy: 100;w=60` | Optional (emerging standard) |

## Decision Tree

```
START
├── Single server instance?
│   ├── YES → In-memory rate limiter (Map/dict with TTL cleanup)
│   │   ├── Need burst tolerance? → Token Bucket
│   │   └── Need smooth constant rate? → Leaky Bucket
│   └── NO (distributed) ↓
├── Have Redis available?
│   ├── YES ↓
│   │   ├── Need exact per-second accuracy (billing/compliance)?
│   │   │   ├── YES → Sliding Window Log (Redis Sorted Set + ZRANGEBYSCORE)
│   │   │   └── NO ↓
│   │   ├── Need minimal memory at scale (>100K keys)?
│   │   │   ├── YES → GCRA (single timestamp per key)
│   │   │   └── NO ↓
│   │   └── DEFAULT → Sliding Window Counter (best accuracy/memory trade-off)
│   └── NO Redis ↓
├── Database-backed acceptable (higher latency)?
│   ├── YES → Fixed Window with SQL counter + row-level lock
│   └── NO ↓
└── DEFAULT → Use API gateway rate limiting (Kong, NGINX, Envoy, Cloudflare)
```

## Step-by-Step Guide

### 1. Choose your rate limit key strategy

Decide how to identify clients. The key determines who gets throttled together. [src1]

| Strategy | Key Pattern | Pros | Cons |
|---|---|---|---|
| Per-IP | `ratelimit:{ip}` | No auth required | Fails behind NAT/shared IPs |
| Per-API-key | `ratelimit:{api_key}` | Accurate per-customer | Requires authentication |
| Per-user | `ratelimit:{user_id}` | Precise per-account | Requires auth + session |
| Per-endpoint | `ratelimit:{ip}:{method}:{path}` | Granular control | More keys = more memory |
| Tiered | `ratelimit:{tier}:{user_id}` | Differentiated limits | Complex configuration |

**Verify**: Define your key format before writing any code. For most APIs: `ratelimit:{api_key}:{endpoint}`.

### 2. Implement the rate limiter with Redis Lua script

Use a Lua script for atomicity — this prevents race conditions that plague get-then-set approaches. [src2] [src5]

```lua
-- sliding_window_counter.lua
-- Atomic sliding window counter rate limiter
-- KEYS[1] = rate limit key (e.g., "ratelimit:user123:/api/data")
-- ARGV[1] = window size in seconds
-- ARGV[2] = max requests per window
-- ARGV[3] = current timestamp (seconds)
-- Returns: {allowed (0/1), remaining, retry_after}

local key = KEYS[1]
local window = tonumber(ARGV[1])
local max_requests = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

local current_key = key .. ":" .. math.floor(now / window)
local previous_key = key .. ":" .. (math.floor(now / window) - 1)

local current_count = tonumber(redis.call("GET", current_key) or "0")
local previous_count = tonumber(redis.call("GET", previous_key) or "0")

-- Weight previous window by remaining fraction
local elapsed = now % window
local weight = (window - elapsed) / window
local estimated_count = math.floor(previous_count * weight) + current_count

if estimated_count >= max_requests then
    local retry_after = window - elapsed
    return {0, 0, retry_after}
end

redis.call("INCR", current_key)
redis.call("EXPIRE", current_key, window * 2)

local remaining = max_requests - estimated_count - 1
return {1, remaining, 0}
```

**Verify**: `redis-cli EVAL "$(cat sliding_window_counter.lua)" 1 "test:key" 60 100 $(date +%s)` -> expected: `1` (allowed), remaining count, `0` (no retry needed)

### 3. Add HTTP response headers

Always include rate limit headers in every response, not just 429 responses. This lets clients self-throttle. [src3] [src6]

```javascript
// Express.js middleware — attaching rate limit headers
function setRateLimitHeaders(res, limit, remaining, resetTimestamp, retryAfter) {
  res.set('X-RateLimit-Limit', String(limit));
  res.set('X-RateLimit-Remaining', String(Math.max(0, remaining)));
  res.set('X-RateLimit-Reset', String(resetTimestamp));
  if (retryAfter > 0) {
    res.set('Retry-After', String(retryAfter));
  }
}
```

**Verify**: `curl -i https://your-api.com/endpoint` -> check for `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` headers in response.

### 4. Handle rate limit exceeded responses

Return a structured error body with the 429 status code. [src3]

```json
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Please retry after 30 seconds.",
    "retry_after": 30,
    "limit": 100,
    "window": "60s",
    "documentation_url": "https://docs.your-api.com/rate-limits"
  }
}
```

**Verify**: Send requests exceeding your limit -> should receive HTTP 429 with `Retry-After` header and structured JSON body.

### 5. Implement graceful degradation for rate limiter failures

If Redis is unreachable, fail open (allow requests) or fail closed (reject all) depending on your security requirements. [src8]

```javascript
// Fail-open strategy — allow requests if rate limiter is down
async function checkRateLimit(key, limit, window) {
  try {
    const result = await redis.eval(luaScript, 1, key, window, limit, Date.now() / 1000);
    return { allowed: result[0] === 1, remaining: result[1], retryAfter: result[2] };
  } catch (err) {
    console.error('Rate limiter unavailable, failing open:', err.message);
    // Fail open — allow request but log for monitoring
    return { allowed: true, remaining: -1, retryAfter: 0, degraded: true };
  }
}
```

**Verify**: Stop Redis -> send API request -> should succeed (fail-open) with monitoring alert logged.

## Code Examples
<!-- Keep inline examples <=15 lines. For longer scripts, extract to scripts/ subdirectory
     and link: "Full script: [name.ext](scripts/name.ext) (N lines)" -->

### Node.js (Express): Token Bucket with Redis

> Full script: [node-js-express-token-bucket-with-redis.js](scripts/node-js-express-token-bucket-with-redis.js) (53 lines)

```javascript
// Input:  HTTP request to any Express route
// Output: 429 if rate limited, next() if allowed
// Requires: ioredis ^5.0.0
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
# ... (see full script)
```

### Python (FastAPI): Sliding Window Counter with Redis

> Full script: [python-fastapi-sliding-window-counter-with-redis.py](scripts/python-fastapi-sliding-window-counter-with-redis.py) (55 lines)

```python
# Input:  HTTP request to any FastAPI route
# Output: 429 JSONResponse if rate limited, None if allowed
# Requires: redis>=5.0.0, fastapi>=0.100.0
import time, redis.asyncio as aioredis
from fastapi import Request, HTTPException
# ... (see full script)
```

### Go (net/http): GCRA with Redis

> Full script: [go-net-http-gcra-with-redis.go](scripts/go-net-http-gcra-with-redis.go) (74 lines)

```go
// Input:  HTTP request to any Go HTTP handler
// Output: 429 if rate limited, passes to next handler if allowed
// Requires: go-redis/redis/v9
package ratelimit
import (
# ... (see full script)
```

### In-Memory Fallback (Node.js): No Redis Dependency

> Full script: [in-memory-fallback-node-js-no-redis-dependency.js](scripts/in-memory-fallback-node-js-no-redis-dependency.js) (36 lines)

```javascript
// Input:  HTTP request — single-instance only
// Output: { allowed: bool, remaining: int, retryAfter: int }
// Zero dependencies — for development/single-process
class InMemoryRateLimiter {
  constructor({ capacity = 100, windowMs = 60000 }) {
# ... (see full script)
```

## Anti-Patterns

### Wrong: Non-atomic get-then-set in distributed environment

```javascript
// BAD — race condition: two concurrent requests both read count=99 (limit=100),
// both increment to 100, both pass — allowing 101 requests through
async function checkRateLimit(key) {
  const count = await redis.get(key);       // read
  if (count >= 100) return false;
  await redis.incr(key);                    // write — NOT atomic with read!
  return true;
}
```

### Correct: Atomic Lua script or INCR-first pattern

```javascript
// GOOD — atomic: INCR returns new value, single round-trip, no race condition
async function checkRateLimit(key, limit, windowSec) {
  const count = await redis.incr(key);      // atomic increment
  if (count === 1) await redis.expire(key, windowSec);  // set TTL on first request
  return count <= limit;
}
// Even better: use the Lua script from Step 2 for sliding window accuracy
```

### Wrong: Rate limiting after expensive operations

```javascript
// BAD — database query runs BEFORE rate limit check
// Attacker sends 10,000 requests, all hit the database
app.get('/api/search', async (req, res) => {
  const results = await db.query(req.query.q);  // expensive!
  if (await isRateLimited(req.ip)) {
    return res.status(429).json({ error: 'Rate limited' });
  }
  res.json(results);
});
```

### Correct: Rate limit as the first middleware

```javascript
// GOOD — rate limit check happens before any expensive operation
app.get('/api/search',
  rateLimiter({ limit: 100, window: 60 }),  // first!
  async (req, res) => {
    const results = await db.query(req.query.q);
    res.json(results);
  }
);
```

### Wrong: Missing Retry-After header on 429 responses

```javascript
// BAD — client has no idea when to retry, so it retries immediately in a tight loop
app.use((req, res, next) => {
  if (isRateLimited(req)) {
    return res.status(429).json({ error: 'Too many requests' });
    // Missing: Retry-After header!
  }
  next();
});
```

### Correct: Always include Retry-After and rate limit headers

```javascript
// GOOD — client knows exactly when to retry, can implement backoff
app.use((req, res, next) => {
  const result = checkRateLimit(req);
  if (!result.allowed) {
    res.set('Retry-After', String(result.retryAfter));
    res.set('X-RateLimit-Limit', '100');
    res.set('X-RateLimit-Remaining', '0');
    return res.status(429).json({
      error: 'Too many requests',
      retry_after: result.retryAfter
    });
  }
  next();
});
```

## Common Pitfalls

- **Fixed window boundary burst**: Two bursts of `limit` requests at the end and start of adjacent windows allows `2x limit` in a short period. Fix: use sliding window counter or GCRA instead of fixed window. [src1]
- **In-memory limiter in clustered/multi-instance deployment**: Each instance tracks its own counts, so actual throughput is `limit * instance_count`. Fix: use Redis or another shared store for distributed deployments. [src2]
- **Key collision with shared IPs**: Corporate NAT, VPNs, and mobile carriers can put thousands of users behind one IP. Fix: combine IP with `X-Forwarded-For` header chain, or prefer API key / user ID as the rate limit key. [src6]
- **Redis failure cascading**: If Redis goes down and you fail-closed, your entire API becomes unavailable. Fix: implement fail-open with circuit breaker and local in-memory fallback. [src8]
- **Clock skew in distributed systems**: Different server clocks cause inconsistent window boundaries. Fix: use Redis `TIME` command as the single time source in Lua scripts, or use NTP-synced monotonic clocks. [src5]
- **Rate limiting websocket connections by message count**: HTTP rate limiting patterns don't apply to persistent connections. Fix: implement per-connection message rate limiting with token bucket at the connection handler level. [src7]
- **Forgetting to rate limit by endpoint granularity**: A global per-user limit of 100 req/min means a heavy `/search` endpoint competes with cheap `/health`. Fix: use tiered limits — e.g., 10 req/min for `/search`, 1000 req/min for `/health`. [src8]

## Diagnostic Commands

```bash
# Check current rate limit state for a key in Redis
redis-cli GET "rl:user123:/api/data:$(date +%s | awk '{print int($1/60)}')"

# Monitor rate limit operations in real-time
redis-cli MONITOR | grep "rl:"

# Count total rate limit keys in Redis
redis-cli DBSIZE
redis-cli KEYS "rl:*" | wc -l  # WARNING: blocking — use SCAN in production

# Test rate limiting with curl (send 10 rapid requests)
for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code} " http://localhost:3000/api/test -H "X-API-Key: test123"; done

# Check Redis memory usage for rate limit keys
redis-cli MEMORY USAGE "rl:user123:/api/data"

# Verify Retry-After header on 429 response
curl -i http://localhost:3000/api/test -H "X-API-Key: test123" 2>&1 | grep -E "(HTTP|Retry-After|X-RateLimit)"
```

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Public-facing API that must handle untrusted clients | Internal microservice-to-microservice calls with trusted callers | Circuit breaker + bulkhead pattern |
| Need to enforce fair usage across API consumers | Single-user CLI tool or batch processing job | Simple concurrency limiter (semaphore) |
| Protecting expensive backend resources (DB, ML model) | Network-layer DDoS attack (volumetric) | CDN/WAF rate limiting (Cloudflare, AWS Shield) |
| Multi-tenant SaaS with per-customer quotas | Real-time streaming/WebSocket with persistent connections | Per-connection message throttling |
| Compliance requires audit trail of usage per customer | Static asset serving (images, CSS, JS) | CDN caching + edge rules |

## Important Caveats

- The IETF `RateLimit` header fields (draft-ietf-httpapi-ratelimit-headers) are still in draft status as of February 2026 — the `X-RateLimit-*` prefix remains the de facto standard but may change when the RFC is finalized
- Redis Cluster mode requires all keys in a Lua script to hash to the same slot — use hash tags (`{user123}`) to ensure co-location of related rate limit keys
- GCRA's theoretical arrival time (TAT) can drift if server clocks are adjusted (NTP jumps) — use monotonic time sources where available
- Rate limiting alone is not a security solution — it must be combined with authentication, input validation, and WAF rules for comprehensive API protection
- Cloud provider managed rate limiters (AWS API Gateway, Cloudflare Rate Limiting, GCP Apigee) have different billing models — some charge per evaluation, not just per rejection

## Related Units
<!-- Generated from related_kos frontmatter -->

- [REST API Pagination](/software/patterns/rest-pagination/2026)
- [API Versioning Strategies](/software/patterns/api-versioning/2026)
- [API Key Management](/software/patterns/api-key-management/2026)
- [Circuit Breaker Pattern](/software/patterns/circuit-breaker/2026)
- [Retry with Exponential Backoff](/software/patterns/retry-exponential-backoff/2026)
- [Rate Limiter System Design](/software/system-design/rate-limiter/2026)