Rate Limiting Architecture for High-Traffic APIs: From Edge to Origin

· Sarah Mitchell · 10 min read

Every API that faces the public internet will eventually face abuse. Whether the source is a misconfigured client retry loop, a credential stuffing attack, or a competitor running aggressive scraping, the result is the same: your origin servers drown under traffic that your paying customers never asked for. HTTP 429 Too Many Requests is the response code that tells abusive clients to back off, but returning a 429 is the easy part. The hard problem is deciding where to count, what to count, and how to coordinate rate limits across a distributed CDN with hundreds of edge nodes processing millions of requests per second.

Why Rate Limiting at the CDN Edge Is Different from Origin Rate Limiting

Rate limiting is the practice of controlling the number of requests a client can make to an API within a defined time window. At its simplest, rate limiting is a counter and a clock. The complexity comes from distributed systems: when your API traffic arrives at dozens of edge servers across multiple continents, there is no single counter to increment.

Origin-level rate limiting operates with full application context. The origin server knows the authenticated user, their subscription tier, their historical usage patterns, and the specific endpoint being called. It can make fine-grained decisions: this free-tier user gets 100 requests per minute to the search endpoint, while this enterprise customer gets 10,000 requests per minute with burst capacity to 15,000.

Edge-level rate limiting operates with limited context but massive scale. The edge server sees the client IP, request headers, the URL path, and possibly an API key or bearer token. It cannot query the user database, check subscription tiers, or evaluate complex business rules without a round-trip to the origin. What it can do is block volumetric attacks, enforce per-IP connection limits, and throttle requests that exceed coarse-grained thresholds — all before a single byte reaches your origin infrastructure.

Two-Tier Rate Limiting Architecture Two-Tier Rate Limiting: Edge + Origin Clients Legit user Legit user Scraper DDoS bot Retry loop CDN Edge Layer Coarse-grained enforcement IP-based: 1000 req/min API key: 5000 req/min Path: /login 10 req/min Geo: block + throttle Origin Layer Fine-grained enforcement User tier quotas Endpoint budgets Tenant isolation Business rule limits 429 429 429 (quota) Blocks 60–90% of abuse Enforces business fairness
Figure 1: Two-tier rate limiting architecture — the CDN edge filters volumetric abuse while the origin enforces per-user business quotas.

Rate Limiting Algorithms: Choosing the Right Counter

The choice of rate limiting algorithm determines the behavior clients experience when they approach or exceed their limits. Each algorithm makes different trade-offs between burst tolerance, memory usage, and implementation complexity. Understanding these trade-offs is critical for engineers designing API acceleration infrastructure.

Performance Comparison: Edge vs Origin Processing
Metric Edge Processing Origin Processing Improvement
Latency (p50)8–15 ms120–300 ms15–20x faster
Cache Hit Ratio85–95%N/AReduces origin load
TLS Handshake1–5 ms (nearby PoP)50–150 ms10–30x faster
Bandwidth Cost$0.01–0.04/GB$0.05–0.12/GB3–5x cheaper
Availability99.99% (multi-PoP)99.9% (single DC)10x fewer outages

Fixed Window Counter

The simplest approach: divide time into fixed windows (e.g., one-minute intervals) and count requests in each window. When the count exceeds the limit, reject subsequent requests until the next window begins.

# Fixed window counter (Redis pseudocode)
def check_rate_limit(client_id, limit, window_seconds):
    key = f"ratelimit:{client_id}:{int(time() / window_seconds)}"
    current = redis.incr(key)
    if current == 1:
        redis.expire(key, window_seconds)
    return current <= limit

The fixed window has a well-known flaw: a client can send limit requests at the end of one window and limit more at the start of the next, effectively doubling their rate for a brief period. For a limit of 100 requests per minute, a client could send 200 requests in a 2-second span straddling the window boundary.

Sliding Window Log

The sliding window log records the timestamp of every request and counts how many fall within the trailing window. This eliminates the boundary spike problem but requires storing individual timestamps, which consumes more memory than a simple counter.

# Sliding window log (Redis sorted set)
def check_rate_limit(client_id, limit, window_seconds):
    now = time()
    key = f"ratelimit:{client_id}"
    window_start = now - window_seconds

    # Remove expired entries
    redis.zremrangebyscore(key, 0, window_start)

    # Count entries in window
    current = redis.zcard(key)
    if current < limit:
        redis.zadd(key, {str(now): now})
        redis.expire(key, window_seconds)
        return True
    return False

At CDN edge scale, the memory cost of storing individual timestamps makes this approach impractical for IP-based limiting where millions of unique IPs might be tracked. It works well for API-key-based limiting where the number of unique keys is bounded.

Token Bucket

The token bucket algorithm models rate limiting as a bucket that holds tokens. Tokens are added at a constant rate (the refill rate), and each request consumes one token. If the bucket is empty, the request is rejected. The bucket has a maximum capacity (the burst size), allowing short bursts of traffic above the sustained rate as long as the bucket has accumulated tokens during quiet periods.

# Token bucket algorithm
def check_rate_limit(client_id, rate, burst):
    now = time()
    bucket = get_bucket(client_id)

    # Calculate tokens to add since last check
    elapsed = now - bucket.last_refill
    tokens_to_add = elapsed * rate
    bucket.tokens = min(burst, bucket.tokens + tokens_to_add)
    bucket.last_refill = now

    if bucket.tokens >= 1:
        bucket.tokens -= 1
        save_bucket(client_id, bucket)
        return True
    return False

Token bucket is the most common algorithm at CDN edges because it handles bursty traffic gracefully. A legitimate mobile client that opens an app and immediately fires 10 API calls is allowed through if the bucket has accumulated tokens during the idle period, rather than being penalized for a burst that falls within the sustained rate over a longer interval.

Leaky Bucket

The leaky bucket smooths traffic to a constant output rate. Requests enter the bucket and are processed at a fixed rate. If the bucket overflows (queue full), requests are dropped. Unlike token bucket, leaky bucket does not allow bursts — the output rate is strictly constant.

Leaky bucket is suitable for protecting backend services that cannot tolerate any burst — database write endpoints, payment processing APIs, or services with strict per-second throughput limits from third-party dependencies.

AlgorithmBurst HandlingMemory per ClientBoundary ProblemBest For
Fixed WindowNone (hard cutoff)O(1) counterYes (2x burst)Simple IP-based limits
Sliding Window LogNoneO(n) timestampsNoLow-volume API key limits
Sliding Window CounterWeighted blendO(1) two countersApproximatedGeneral purpose
Token BucketAllowed (burst size)O(1) tokens + timestampNoBursty client traffic
Leaky BucketQueued or droppedO(n) queueNoConstant-rate backends

How Should APIs Communicate Rate Limit Status to Clients?

Transparent rate limit communication is critical for API usability. Clients that can see their remaining quota and the reset time can self-throttle before hitting the limit, reducing unnecessary 429 responses and improving the experience for both the client developer and the API infrastructure.

The de facto standard uses three response headers included in every API response, not just 429 responses:

HTTP/1.1 200 OK
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4987
X-RateLimit-Reset: 1727740800
Content-Type: application/json

When the limit is exceeded, the API returns HTTP 429 with a Retry-After header:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1727740800
Content-Type: application/json

{"error": "rate_limit_exceeded", "message": "Too many requests. Retry after 30 seconds."}

The IETF has published a draft specification (draft-ietf-httpapi-ratelimit-headers) proposing standardized header names without the X- prefix: RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. The draft also supports quota policies with multiple limits (e.g., 100 per second and 5000 per hour) expressed in a structured header format. Adopting both the legacy X- prefixed and the standardized names during the transition period ensures broad client compatibility.

Implementing Rate Limiting at the CDN Edge with Nginx

Nginx's built-in rate limiting module (ngx_http_limit_req_module) uses a leaky bucket algorithm and is the most common edge-level rate limiter in production. Configuring it correctly requires understanding the interaction between rate, burst, and nodelay parameters.

# /etc/nginx/conf.d/rate-limiting.conf

# Define rate limit zones
# $binary_remote_addr uses 4 bytes per IPv4, saving memory vs $remote_addr
limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=50r/s;
limit_req_zone $http_x_api_key zone=api_key_limit:20m rate=100r/s;
limit_req_zone $binary_remote_addr zone=login_limit:5m rate=5r/m;

# Custom 429 response
limit_req_status 429;

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # General API rate limit: 50 req/s per IP with burst of 20
    location /api/ {
        limit_req zone=ip_limit burst=20 nodelay;
        limit_req zone=api_key_limit burst=50 nodelay;

        # Add rate limit headers to response
        add_header X-RateLimit-Limit "50" always;
        # Note: Nginx doesn't natively track remaining count
        # Use OpenResty/Lua for dynamic remaining calculation

        proxy_pass http://backend;
    }

    # Strict rate limit for authentication endpoints
    location /api/auth/login {
        limit_req zone=login_limit burst=2 nodelay;
        proxy_pass http://backend;
    }

    # Custom 429 error page with Retry-After
    error_page 429 = @rate_limited;
    location @rate_limited {
        default_type application/json;
        add_header Retry-After 30 always;
        add_header X-RateLimit-Remaining 0 always;
        return 429 '{"error":"rate_limit_exceeded","retry_after":30}';
    }
}

The burst parameter defines how many excess requests are queued (or with nodelay, served immediately) before Nginx begins rejecting. Without nodelay, burst requests are queued and processed at the defined rate, introducing latency. With nodelay, burst requests are served immediately but still consume burst capacity, meaning subsequent bursts within the refill period will be rejected. For API endpoints where latency matters, nodelay is almost always the right choice.

The zone size (10m) determines how many unique keys can be tracked. Each $binary_remote_addr entry uses approximately 64 bytes, so a 10 MB zone holds approximately 160,000 unique IP addresses. For edge servers handling traffic from millions of unique IPs, increase the zone size or use multiple zones with different tracking granularity. Nginx performance optimization becomes critical when rate limiting is applied at edge scale.

Distributed Rate Limiting: Coordinating Across Edge Nodes

The fundamental challenge of edge rate limiting is that a single client's requests are distributed across multiple edge nodes. If a client has a limit of 1000 requests per minute and your CDN has 10 edge PoPs, each PoP might see only 100 requests. Without coordination, each PoP would track 100/1000 and allow all requests, even if the global total has reached 1000.

Distributed Rate Limiting Coordination Strategies Distributed Rate Limiting Strategies 1. Local Counters Global limit / N nodes PoP A PoP B PoP C 333 each (1000/3) 2. Centralized Counter Single Redis cluster Redis PoP PoP 3. Gossip Protocol Peer-to-peer sync PoP A PoP B PoP C Pros Zero network latency Simple implementation Cons Inaccurate with uneven load Over-provisions by N factor Pros Exact global count Consistent enforcement Cons Adds 1–5ms latency per req Single point of failure Pros Eventually consistent No central bottleneck Cons Temporary over-admission Complex to implement
Figure 2: Three strategies for coordinating rate limits across distributed CDN edge nodes, each with different accuracy and latency trade-offs.

Local Counters with Static Division

The simplest approach divides the global limit by the number of edge nodes. If the global limit is 1000 requests per minute across 10 nodes, each node enforces 100 requests per minute. This requires no network coordination and adds zero latency, but it assumes even traffic distribution. In practice, traffic is rarely even — a user in New York sends most requests to the US-East edge, not to the APAC nodes. The result is that some nodes reject legitimate traffic while others have unused quota.

Centralized Counter with Redis

A centralized Redis cluster maintains global counters for each rate-limited key. Every edge node increments the counter via a Redis call before processing each request. Redis operations (INCR + EXPIRE) take under 1 millisecond in the same data center, but cross-region latency adds 10 to 50 milliseconds. The solution is to deploy Redis clusters regionally and accept that cross-region consistency is approximate rather than exact.

Gossip-Based Synchronization

Edge nodes periodically share their local counters with neighboring nodes using a gossip protocol. Each node maintains its own counter and a merged view of nearby nodes' counters. The merged view converges to the approximate global count within a few gossip rounds (typically 2 to 5 seconds). This approach trades perfect accuracy for low latency and no single point of failure.

Client-Side Retry Strategies: Exponential Backoff and Jitter

When a client receives a 429 response, the retry strategy determines whether the system recovers gracefully or spirals into a retry storm. The bot management layer at the edge bears the brunt of poorly implemented retries.

Deterministic Retry with Retry-After

The best-case scenario: the client reads the Retry-After header and waits the specified duration before retrying. This gives the server direct control over client retry timing and prevents premature retries from consuming additional quota.

// Client retry logic (pseudocode)
async function apiCallWithRetry(url, options, maxRetries = 3) {
    for (let attempt = 0; attempt < maxRetries; attempt++) {
        const response = await fetch(url, options);

        if (response.status !== 429) return response;

        // Check Retry-After header
        const retryAfter = response.headers.get("Retry-After");
        if (retryAfter) {
            // Could be seconds or HTTP-date
            const delay = isNaN(retryAfter)
                ? new Date(retryAfter) - Date.now()
                : parseInt(retryAfter) * 1000;
            await sleep(Math.max(delay, 1000));
            continue;
        }

        // Fallback: exponential backoff with jitter
        const baseDelay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
        const jitter = Math.random() * 1000;           // 0-1s random
        await sleep(Math.min(baseDelay + jitter, 32000));
    }
    throw new Error("Rate limit exceeded after max retries");
}

M2M Token Caching

For machine-to-machine (M2M) API integrations, a significant portion of rate limit consumption comes from redundant token requests. Every API call that starts with "get an access token, then call the API" doubles the request count if the token is not cached. The fix is straightforward: cache access tokens until shortly before their expiration (with a buffer of 30 to 60 seconds), and reuse the same token across requests.

This optimization alone can reduce API traffic by 30 to 50 percent for integration patterns where token lifetime is measured in hours but API calls happen every few seconds. The cache key should include the client ID, requested scopes, and target audience to avoid serving a token with insufficient permissions.

Advanced: Adaptive Rate Limiting with Edge Intelligence

Static rate limits (fixed numbers per window) work for predictable traffic, but real-world API traffic is anything but predictable. Flash sales, breaking news events, and viral content can cause legitimate traffic to spike 10x above normal levels. Adaptive rate limiting adjusts thresholds based on real-time system health rather than fixed configuration.

An adaptive rate limiter at the edge monitors origin server health signals: response time percentiles, error rates, queue depth, and CPU utilization. When origin health degrades, the edge automatically tightens rate limits to shed load before the origin fails. When the origin recovers, limits relax back to their normal levels.

# Adaptive rate limiting configuration (pseudocode)
adaptive_rate_limit:
  base_rate: 1000        # requests per minute per client
  min_rate: 100           # floor during degradation
  health_signals:
    - metric: origin_p99_latency
      threshold: 500ms    # if p99 > 500ms, start reducing
      weight: 0.4
    - metric: origin_error_rate
      threshold: 5%       # if errors > 5%, reduce aggressively
      weight: 0.4
    - metric: origin_cpu
      threshold: 80%
      weight: 0.2
  adjustment_interval: 10s  # re-evaluate every 10 seconds
  recovery_rate: 50         # increase limit by 50/interval during recovery

The monitoring pipeline for adaptive limiting must be low-latency: health signals should arrive at edge nodes within 5 seconds of the underlying metric changing. Log analysis feeds into the health model, identifying patterns like cascading failures where one backend's degradation increases load on others.

Rate limiting is a spectrum, not a binary. At one end, static IP-based limits at the CDN edge block the obvious abuse. At the other end, adaptive per-user limits at the origin enforce business fairness under varying load. A well-designed API needs both: the edge stops the flood so the origin can focus on the customers who matter. The coordination between layers — shared security policies, consistent rate limit headers, and intelligent retry guidance — determines whether your rate limiting protects your system or frustrates your users. Build both layers, instrument everything, and remember that 503 Service Unavailable is what happens when your rate limiting does not work.

External References

References & Standards: RFC 7234 · W3C Web Performance