Background
Sections
IntroductionRequirements & Problem AnalysisConstraints & AssumptionsEstimation TechniquesFunctional vs Non-Functional RequirementsMoSCoW PrioritizationSystem Design FundamentalsArchitecture DiagramClass DiagramComponent DiagramData Flow Diagram (DFD)ER Diagram (Entity-Relationship Diagram)High Level Design (HLD)Low Level Design (LLD)Sequence DiagramState DiagramUse Case DiagramData StorageDocument StoresFile StorageGraph DatabasesIn-Memory DatabasesKey-Value StoresNewSQLNoSQL DatabasesObject StorageSQL Databases (RDBMS)Time-Series DatabasesWide-Column StoresDatabase ConceptsACID PropertiesCAP TheoremConsistency ModelsIndexingNormalization & DenormalizationReplicationSharding & PartitioningTransactions & Isolation LevelsScalabilityAuto-Scaling & ElasticityConsensus & Leader ElectionLoad BalancingReplication & Read ReplicasSharding & PartitioningVertical vs Horizontal ScalingAvailability & ReliabilityBackup & Data DurabilityCircuit BreakerData ConsistencyDisaster RecoveryFault Tolerance & FailoverGraceful DegradationHigh AvailabilityNetworkingCDNDNSFirewalls & VPNHTTP & HTTPSLoad Balancer & Reverse ProxyTCP/IP & UDPWebSocketsCachingCache InvalidationCache Read/Write PatternsCaching LayersEviction PoliciesRedis vs MemcachedMessaging & CommunicationDead-Letter QueueIdempotencyKafka vs RabbitMQ vs SQSMessage QueuesPub/SubCompute & ServicesAPI GatewayContainers & OrchestrationMonolith vs MicroservicesServerlessService DiscoveryService MeshWeb Server & App ServerAPI DesignAPI Versioning & IdempotencyAuthentication & AuthorizationGraphQLgRPCPaginationRate Limiting & ThrottlingRESTSecurityAuthentication & AuthorizationData PrivacyEncryptionInput Validation & InjectionOAuth2 & JWTSecrets ManagementXSS & CSRFStorage & File SystemsBackup & RetentionBlock vs File vs Object StorageData Lakes & WarehousesDistributed File SystemsEphemeral StorageObservability & MonitoringDistributed TracingHealth ChecksLoggingMetricsSLI, SLO, SLADesign PatternsBulkhead PatternCircuit Breaker PatternCreational PatternsRate Limiter PatternRetry PatternStructural & Behavioral Patterns

Rate Limiter Pattern

9 min read

In a Nutshell

The rate limiter pattern controls the rate at which operations are allowed to proceed — capping how many requests, calls, or actions can happen in a given time window. It's a protective pattern that shows up on both sides of a system: as a server-side control to protect your service from being overwhelmed by clients (abuse, traffic spikes, runaway loops), and as a client-side control to keep your own outbound calls within a downstream service's limits (so you don't get throttled or banned). This topic covers rate limiting as an implementable pattern — the algorithms and where to apply them — complementing the API-focused treatment in Topic 11 — Rate Limiting & Throttling.

2D minimalistic diagram showing a rate limiter as a gate controlling flow: a stream of requests arriving, the limiter allowing them through up to a set rate (green) and rejecting or delaying the excess (red), with a token-bucket meter showing available capacity, illustrating controlled throughput in both server-side and client-side directions

How It Actually Works

Two Directions of Rate Limiting

Direction Purpose Example
Server-side (inbound) Protect your service from clients "100 req/min per user" to prevent abuse/overload
Client-side (outbound) Respect a downstream's limits Throttle your calls to a third-party API to avoid being banned

The same algorithms apply to both; the direction just changes what you're protecting.

The Core Algorithms

Algorithm How It Works Characteristic
Token Bucket Tokens refill at a rate; each request spends one; bucket caps burst Allows bursts, enforces average rate — the popular default
Leaky Bucket Requests queue and drain at a fixed rate Smooths output to a constant rate
Fixed Window Count requests per fixed interval Simple; boundary-burst problem
Sliding Window Rolling window count (log or weighted counter) Accurate, avoids boundary bursts

Token Bucket — The Go-To

Because it permits controlled bursts while enforcing a sustained average rate, token bucket is the most common choice:

class TokenBucket:
    def __init__(self, capacity, refill_rate):
        self.capacity = capacity          # max burst
        self.tokens = capacity
        self.refill_rate = refill_rate    # tokens per second
        self.last = time.time()

    def allow(self, cost=1):
        now = time.time()
        # Refill based on elapsed time, capped at capacity
        self.tokens = min(self.capacity,
                          self.tokens + (now - self.last) * self.refill_rate)
        self.last = now
        if self.tokens >= cost:
            self.tokens -= cost
            return True                   # allowed
        return False                      # rate limited

A client can burst up to capacity immediately, then is limited to refill_rate sustained — matching real usage where occasional bursts are fine but sustained excess isn't.

Distributed Rate Limiting

In a multi-server deployment, the limit must be global, not per-server — otherwise a client gets limit × server_count. The counter/bucket state must be shared, typically in Redis with atomic operations:

Naive: each of 5 servers enforces "100/min" locally → client gets 500/min. 💥
Correct: shared Redis token bucket (atomic Lua script) → global 100/min. ✅

Client-Side: Respecting Downstream Limits

When calling a rate-limited service, the pattern protects you from being throttled:

- Pace outbound calls with a local token bucket sized to the API's limit.
- Honor 429 responses: read Retry-After and back off (see retry-pattern.md).
- Queue and drain requests at the allowed rate rather than bursting and
  getting rejected.
This keeps you a good citizen and avoids bans/throttling.
Rate limiter:  cap the RATE of operations (per client/key/global).
Throttling:    slow down / delay excess (a rate-limiter response mode).
Load shedding: drop LOW-PRIORITY work when the SYSTEM is overloaded
               (system-driven, see graceful-degradation.md).
Bulkhead:      isolate RESOURCES so one workload can't exhaust others.

These compose: rate limiting caps per-client volume, bulkheads isolate resources, and load shedding drops low-priority work under system-wide overload.

2D minimalistic diagram of the token bucket algorithm applied in both directions: a bucket refilled by a steady token drip at a fixed rate, with requests each consuming a token to pass; on one side it gates inbound client requests (server protection), on the other side it paces outbound calls to a downstream API (respecting its limit), illustrating the same mechanism serving both purposes

Seeing It in Action

Scenario: Rate limiting in both directions for an API integration service.

# SERVER-SIDE — protect our API from clients (distributed via Redis).
#   Per-API-key token bucket enforced at the gateway, GLOBAL across servers.
def check_inbound(api_key, plan_rate):
    key = f"ratelimit:{api_key}"
    if redis_token_bucket_allow(key, capacity=plan_rate, refill=plan_rate/60):
        return True                        # within quota → proceed
    raise RateLimited(retry_after=8)       # 429 + Retry-After

# CLIENT-SIDE — respect a third-party API's limit (say, 10 req/sec).
#   Pace our OUTBOUND calls so we're never throttled or banned.
outbound = TokenBucket(capacity=10, refill_rate=10)   # match their limit

def call_third_party(request):
    while not outbound.allow():
        time.sleep(0.05)                   # wait for a token (pace ourselves)
    resp = third_party.send(request)
    if resp.status == 429:                 # they throttled us anyway
        time.sleep(resp.retry_after)       # honor Retry-After, back off
        return call_third_party(request)   # retry after backoff
    return resp

# Why both directions matter:
#  Inbound:  a buggy client looping infinitely hits its per-key limit → gets
#            429s → can't overwhelm our service or degrade other customers.
#            Distributed Redis state means it can't bypass the limit by
#            spreading calls across our servers.
#  Outbound: we never exceed the third party's 10/sec → we're never banned
#            or throttled, and our integration stays reliable. If they
#            throttle us anyway, we honor Retry-After instead of hammering.

Why rate limiting is a fundamental protective pattern in both directions: rate limiting is often thought of only as a server-side defense — capping how much any one client can do so that abuse, bugs, or traffic spikes can't overwhelm your service or let one noisy client degrade everyone else. That inbound use is essential (and, done right, must be distributed — enforced against shared state so a client can't multiply its allowance by spreading requests across your servers, and transparent via 429/Retry-After so well-behaved clients self-regulate). But the same pattern is just as important pointed outward: whenever your system calls a rate-limited dependency — a third-party API, a partner service, even an internal service with quotas — you need to pace your own outbound calls to stay within the allowed rate, or you'll get throttled, rejected, or banned, making your integration unreliable. The token bucket algorithm serves both directions identically because the underlying need is the same: allow controlled bursts up to a capacity while enforcing a sustained average rate. And rate limiting doesn't stand alone — it composes with the other resilience patterns into a coherent defense: rate limiters cap per-client volume, bulkheads isolate resources so one workload can't starve others, circuit breakers stop calling broken dependencies, retries with backoff absorb transient blips (and honor rate-limit responses), and load shedding drops low-priority work under system-wide overload. Each addresses a different way a system gets overwhelmed, and together they let a service protect itself from clients, protect its dependencies from itself, and stay responsive under stress.

Interview Questions

  1. Q: What are the two directions of rate limiting, and why does each matter? Hint: Server-side (inbound) protects your service from clients — capping per-client volume so abuse, bugs, or spikes can't overwhelm you or let one client degrade others. Client-side (outbound) paces your calls to a rate-limited downstream (third-party/partner API) so you don't get throttled or banned. The same algorithms serve both; the direction changes what you're protecting — your service vs your integration's reliability.

  2. Q: Why is the token bucket the most common rate-limiting algorithm? Hint: It allows controlled bursts (up to the bucket capacity) while enforcing a sustained average rate (the refill rate), matching real usage where occasional bursts are fine but sustained excess isn't. It's memory-efficient (just a token count and timestamp) and simple to implement. Leaky bucket smooths to a constant rate (no bursts), and fixed-window has boundary-burst problems.

  3. Q: How do you implement rate limiting correctly across multiple servers? Hint: The limit must be global, using shared state (typically Redis with atomic operations / a Lua token-bucket script), not enforced per-server locally. A naive per-server limit lets a client multiply its allowance by the number of servers (5 servers × 100 = 500). Shared atomic counters/buckets ensure the client sees one global limit regardless of which server handles each request.

  4. Q: When calling a rate-limited third-party API, how does the rate limiter pattern help you? Hint: You pace your outbound calls with a local token bucket sized to the API's limit, so you stay within it and aren't throttled or banned. You also honor 429/Retry-After responses by backing off rather than hammering, and queue/drain requests at the allowed rate instead of bursting and getting rejected. This keeps your integration reliable and makes you a good API citizen.

  5. Q: How does rate limiting compose with bulkheads, circuit breakers, and load shedding? Hint: They address different overload modes and combine into a resilience toolkit: rate limiters cap per-client/global request volume; bulkheads isolate resources so one workload can't exhaust others; circuit breakers stop calling failing dependencies; retries (with backoff/jitter) absorb transient blips and honor rate-limit responses; load shedding drops low-priority work under system-wide overload. Together they let a service protect itself, its dependencies, and its responsiveness.

References

Dive Deeper