Skip to content

Rate Limiting at the API Gateway

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you'll learn about Rate Limiting at Gateway. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Rate limiting at the gateway controls how many requests a client can make within a time window, protecting backend services from abuse, traffic spikes, and denial-of-service attacks.

What You'll Learn

By the end of this lesson, you will implement token bucket and Sliding Window rate limiters, configure per-client limits, and integrate distributed rate limiting with Redis.

Why It Matters

Without gateway rate limiting, a single aggressive client can overwhelm all backend services. Gateway-level limits protect every service behind it.

Real-World Use

A gateway limits anonymous users to 10 requests per minute and authenticated users to 100 requests per minute, with different limits for different API endpoints.

Rate Limiting Architecture

flowchart TD
    Client -->|Request| GW[Gateway]
    GW --> RL{Rate Limiter}
    RL -->|Under Limit| Backend[Backend Service]
    RL -->|Over Limit| Reject[429 Too Many Requests]
    RL --> Redis[(Redis Counter)]
    style Reject fill:#f90,color:#fff

Token Bucket Algorithm

The token bucket is one of the most common rate limiting algorithms. Tokens refill at a constant rate, and each request consumes one token.

# token_bucket.py
import time
from typing import Dict

class TokenBucket:
    def __init__(self, capacity: int, refill_rate: float):
        self.capacity = capacity
        self.refill_rate = refill_rate
        self.tokens: Dict[str, dict] = {}

    def allow(self, client_id: str) -> bool:
        now = time.time()
        if client_id not in self.tokens:
            self.tokens[client_id] = {"tokens": self.capacity, "last_refill": now}

        bucket = self.tokens[client_id]
        elapsed = now - bucket["last_refill"]
        bucket["tokens"] = min(self.capacity, bucket["tokens"] + elapsed * self.refill_rate)
        bucket["last_refill"] = now

        if bucket["tokens"] >= 1:
            bucket["tokens"] -= 1
            return True
        return False

    def remaining(self, client_id: str) -> float:
        if client_id not in self.tokens:
            return self.capacity
        return self.tokens[client_id]["tokens"]

limiter = TokenBucket(capacity=5, refill_rate=1)

for i in range(8):
    allowed = limiter.allow("client_1")
    remaining = limiter.remaining("client_1")
    print(f"Req {i+1}: {'ALLOWED' if allowed else 'BLOCKED'} ({remaining:.1f} tokens left)")

Expected output:

Req 1: ALLOWED (4.0 tokens left)
Req 2: ALLOWED (3.0 tokens left)
Req 3: ALLOWED (2.0 tokens left)
Req 4: ALLOWED (1.0 tokens left)
Req 5: ALLOWED (0.0 tokens left)
Req 6: BLOCKED (0.0 tokens left)
...
Req 8: BLOCKED (0.0 tokens left)

Sliding Window Log

# sliding_window.py
import time
from collections import defaultdict
from typing import Dict, List

class SlidingWindowLimiter:
    def __init__(self, max_requests: int, window_seconds: int):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.requests: Dict[str, List[float]] = defaultdict(list)

    def allow(self, client_id: str) -> bool:
        now = time.time()
        cutoff = now - self.window_seconds

        self.requests[client_id] = [t for t in self.requests[client_id] if t > cutoff]

        if len(self.requests[client_id]) >= self.max_requests:
            return False

        self.requests[client_id].append(now)
        return True

    def remaining(self, client_id: str) -> int:
        now = time.time()
        cutoff = now - self.window_seconds
        recent = [t for t in self.requests.get(client_id, []) if t > cutoff]
        return max(0, self.max_requests - len(recent))

limiter = SlidingWindowLimiter(max_requests=3, window_seconds=10)

clients = ["user_a", "user_b", "user_a", "user_a", "user_a", "user_b"]
for client in clients:
    allowed = limiter.allow(client)
    remaining = limiter.remaining(client)
    print(f"{client}: {'ALLOWED' if allowed else 'BLOCKED'} ({remaining} remaining)")

Expected output:

user_a: ALLOWED (2 remaining)
user_b: ALLOWED (2 remaining)
user_a: ALLOWED (1 remaining)
user_a: ALLOWED (0 remaining)
user_a: BLOCKED (0 remaining)
user_b: ALLOWED (1 remaining)

Distributed Rate Limiting with Redis

# redis_rate_limit.py
import time
import json
from typing import Dict, Optional

class RedisRateLimiter:
    def __init__(self, window_seconds: int = 60):
        self.window_seconds = window_seconds
        self.store: Dict[str, dict] = {}

    def increment(self, key: str) -> dict:
        now = time.time()
        window_key = int(now / self.window_seconds)

        if key not in self.store or self.store[key].get("window") != window_key:
            self.store[key] = {"window": window_key, "count": 0, "start": window_key * self.window_seconds}

        self.store[key]["count"] += 1
        return self.store[key]

    def check(self, key: str, max_count: int) -> dict:
        state = self.increment(key)
        allowed = state["count"] <= max_count
        remaining = max(0, max_count - state["count"])
        reset_in = self.window_seconds - (time.time() - state["start"])

        return {
            "allowed": allowed,
            "remaining": remaining,
            "reset_in_seconds": int(reset_in),
        }

limiter = RedisRateLimiter(window_seconds=60)

for i in range(6):
    result = limiter.check("user:api_key_123", max_count=5)
    print(f"Req {i+1}: {'ALLOWED' if result['allowed'] else 'BLOCKED'} "
          f"(remaining: {result['remaining']}, reset: {result['reset_in_seconds']}s)")

Expected output:

Req 1: ALLOWED (remaining: 4, reset: 59s)
Req 2: ALLOWED (remaining: 3, reset: 59s)
Req 3: ALLOWED (remaining: 2, reset: 59s)
Req 4: ALLOWED (remaining: 1, reset: 59s)
Req 5: ALLOWED (remaining: 0, reset: 59s)
Req 6: BLOCKED (remaining: 0, reset: 59s)

Common Mistakes

1. Single Instance Rate Limiting

Rate limit state stored in memory is lost when the gateway restarts or scales. Use Redis for distributed state.

2. Not Sending Retry-After Headers

Clients need to know when they can retry. Always include the Retry-After header in 429 responses.

3. Same Limits for All Endpoints

Different endpoints have different costs. Apply stricter limits to expensive operations (search, exports) and looser limits to cheap ones (reads).

4. Counting Requests After Processing

Check rate limits early in the pipeline, before expensive processing. Reject over-limit requests immediately.

5. No Client Identification

Without identifying clients (API key, token, IP), rate limiting cannot distinguish between users. Implement proper client identification.

Practice Questions

1. What is the difference between token bucket and sliding window?

Token bucket allows bursts up to capacity and refills steadily. Sliding window counts requests in a moving time window, preventing bursts entirely.

2. Why distributed rate limiting needs Redis?

In-memory rate limit state is lost on restart and not shared across gateway instances. Redis provides shared, persistent counters.

3. What status code should a rate limiter return?

HTTP 429 Too Many Requests with a Retry-After header indicating when the client can retry.

4. How do you set different limits for different client tiers?

Map client tier (free, pro, enterprise) to limit settings. Check the tier before applying the limit.

Challenge

Design a multi-tier rate limiting system with Redis that applies 10 req/min for free, 100 req/min for pro, and 1000 req/min for enterprise, with different limits per endpoint.

FAQ

Can rate limiting prevent DDoS attacks?

It helps but is not sufficient alone. Combined with IP whitelisting, WAF, and CDN-level protection, it forms part of a defense strategy.

Should I rate limit by IP or by user?

By user (API key or token) is preferred. IP-based limiting penalizes users behind shared NAT.

What is a burst limit?

The maximum number of consecutive requests allowed before rate limiting kicks in. Token bucket supports bursts naturally.

How do I handle rate limit state persistence?

Use Redis with appropriate TTLs. For critical limits, consider persistent storage with periodic cleanup.

Can I have different limits for different HTTP methods?

Yes. Apply stricter limits to POST/PUT/DELETE and looser limits to GET requests.

Mini Project: Multi-Tier Rate Limiter

# multi_tier_limiter.py
import time
from typing import Dict

class TierConfig:
    def __init__(self, requests_per_minute: int, burst: int):
        self.rpm = requests_per_minute
        self.burst = burst

class MultiTierRateLimiter:
    def __init__(self):
        self.tiers = {
            "free": TierConfig(10, 5),
            "pro": TierConfig(100, 20),
            "enterprise": TierConfig(1000, 100),
        }
        self.clients: Dict[str, dict] = {}

    def allow(self, client_id: str, tier: str) -> dict:
        config = self.tiers.get(tier, self.tiers["free"])
        now = time.time()

        if client_id not in self.clients:
            self.clients[client_id] = {"tokens": config.burst, "last_refill": now, "tier": tier}

        state = self.clients[client_id]
        elapsed = now - state["last_refill"]
        refill = elapsed * (config.rpm / 60)
        state["tokens"] = min(config.burst, state["tokens"] + refill)
        state["last_refill"] = now

        if state["tokens"] >= 1:
            state["tokens"] -= 1
            return {"allowed": True, "remaining": int(state["tokens"]), "tier": tier}

        return {"allowed": False, "remaining": 0, "tier": tier, "retry_after": 60 / config.rpm}

limiter = MultiTierRateLimiter()
test_clients = [("free_user", "free"), ("pro_user", "pro"), ("enterprise_user", "enterprise")]

for client_id, tier in test_clients:
    results = [limiter.allow(client_id, tier)["allowed"] for _ in range(15)]
    allowed = sum(results)
    print(f"{tier:12s} user: {allowed}/15 allowed")

Expected output:

free         user: 6/15 allowed (burst 5 + ~1 refill)
pro          user: 15/15 allowed (burst 20)
enterprise   user: 15/15 allowed (burst 100)

What's Next

You understand rate limiting. Next, learn about authentication at the gateway, then explore authorization at the gateway.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro