What Is API Rate Limiting?

What Is API Rate Limiting?

A complete, beginner-friendly guide to how APIs protect themselves from overload — the history, the algorithms, the architecture, and how companies like Stripe, GitHub, Shopify, and Netflix actually do it in production.

01 · INTRODUCTION & HISTORY

From The Bakery Sign To The Cloud Gateway

Imagine a small bakery with one oven. If ten customers walk in and each orders fifty loaves at once, the oven catches fire, the baker collapses, and nobody — not even the first customer — gets their bread. Now imagine the baker puts up a simple sign: “Max 5 loaves per customer per hour.” Suddenly the bakery can serve everyone fairly, the oven never overheats, and business keeps running smoothly all day.

API rate limiting is that sign, but for software. It is a technique used by servers to control how many requests a client (a user, an app, another server, or a bot) is allowed to make within a given period of time. If a client tries to exceed that limit, the server simply says “not right now” — usually by returning an HTTP 429 Too Many Requests response — instead of trying to do the work and collapsing under the load.

Where did it come from?

Rate limiting itself is not a new idea — it borrows heavily from much older engineering fields:

  • Telecommunications (1960s–70s): Phone networks used traffic shaping and leaky bucket concepts to prevent one call from hogging a shared line.
  • Networking (1980s–90s): TCP/IP congestion control algorithms (like TCP’s sliding window) inspired the same “windowed counting” ideas used in modern API rate limiters.
  • Early web APIs (2000s): As companies like Amazon, eBay, and later Twitter opened up their platforms to third-party developers, they quickly discovered that a handful of buggy or malicious scripts could bring down an entire service. Rate limiting became a standard defence.
  • The API economy (2010s–today): With the explosion of REST and GraphQL APIs, mobile apps, and now AI systems that call APIs programmatically at machine speed, rate limiting evolved from a “nice-to-have” into a mandatory piece of infrastructure for virtually every public and internal API.
Simple analogy

Think of a highway toll booth that only lets a certain number of cars through per minute. It does not matter how many cars arrive — the booth paces them out so the road behind it never jams up.

1960s

Leaky bucket in telecom

Traffic shaping in phone switching — the direct ancestor of the leaky-bucket algorithm.

1980s

TCP sliding window

Congestion control inspires “count within a window” logic later reused in HTTP rate limiters.

2000s

Public web APIs

Amazon, eBay and Twitter formalise per-key request limits after real-world abuse incidents.

2020s

AI-era traffic

Machine-speed clients and LLM agents make rate limiting a mandatory, documented API contract.

02 · THE PROBLEM & MOTIVATION

Why Servers Cannot Just Handle Every Request

Why can a server not just handle every request as fast as it comes in? Because every server has limited resources: CPU, memory, database connections, network bandwidth, and disk I/O. Without a limiting mechanism, several bad things can happen:

RX

Resource exhaustion

Too many simultaneous requests use up CPU, memory, or database connections, causing slowdowns or crashes for every user — not just the abusive one.

BC

Buggy clients

A client with a bug (like an infinite retry loop) can accidentally hammer your API thousands of times per second.

SC

Scraping & abuse

Bots may try to scrape all your data or brute-force login credentials by making massive numbers of requests.

CO

Cost control

Cloud infrastructure often bills by usage. Unlimited traffic can mean unlimited (and unexpected) bills.

FA

Fairness

Without limits, one “noisy neighbour” client can consume all the capacity, starving out every other well-behaved client.

DO

DoS defence

Rate limiting is a first line of defence against both accidental overload and intentional DoS / DDoS attacks.

“An API without rate limiting is a shared kitchen with no rules about how much stove space anyone gets — eventually, someone burns the whole meal for everybody.”

Rate limiting solves this by acting as a gatekeeper: it decides, before any real work is done, whether a given request is even allowed to proceed. This keeps the system predictable, protects backend resources, and ensures that the API stays available and responsive for everyone.

What happens without it — a concrete scenario

Consider a mid-sized e-commerce API that exposes a /products/search endpoint. It is a Friday during a big sale, and a well-meaning developer at a partner company deploys a new integration with a subtle bug: instead of caching search results for five minutes, it accidentally calls the search endpoint on every single page render, in a loop, for every user browsing their site. Within minutes:

  • Database CPU spikes as thousands of redundant search queries pile up.
  • Response times for every customer — not just that one partner — start climbing from 100 ms to 5+ seconds.
  • The on-call engineer gets paged, and by the time the root cause is found, the checkout flow itself has started timing out, costing real sales.

With rate limiting in place, that same buggy integration would have hit its per-key limit within the first few seconds, started receiving 429 responses, and — assuming the partner’s client had any retry logic at all — the issue would have surfaced as a visible, isolated error in their own logs, long before it ever touched anyone else’s experience. That is the core value proposition: rate limiting converts a shared, cascading failure into a contained, localised one.

03 · CORE CONCEPTS

The Vocabulary That Comes Up Again And Again

Before diving into architecture and algorithms, let us build a shared vocabulary. Each of these terms will come up repeatedly through the rest of the guide.

Rate

The rate is simply “how many requests are allowed” over “how much time.” For example: 100 requests per minute, or 5 requests per second. This is usually written as requests/window.

Window

A window is the time period over which requests are counted — a second, a minute, an hour, or a day. Windows can be fixed (e.g., always aligned to the clock, like “every minute on the minute”) or rolling / sliding (e.g., “the last 60 seconds from right now”).

Quota

A quota is a longer-term allowance, often used for billing tiers — for example, “10,000 API calls per month” as part of a subscription plan. Quotas and rate limits often work together: a rate limit smooths out short-term bursts, while a quota tracks total usage over a longer period.

Throttling

Throttling is the act of actually slowing down or rejecting requests once a limit is reached. Rate limiting is the policy; throttling is the enforcement.

Key (identity)

Every rate limiter needs to know who it is limiting. The “key” is usually one of: an API key, a user ID, an IP address, an OAuth token, or a combination of these. Two requests only count against the same limit if they share the same key.

Burst

A burst is a short spike of requests that arrives faster than the average rate but is still within acceptable limits, provided it does not exceed a maximum burst capacity. Good rate limiters allow reasonable bursts without allowing sustained abuse.

Good rate limiting

  • Predictable, well-communicated limits.
  • Allows small bursts.
  • Gives clear feedback (headers, error codes).

Poor rate limiting

  • Silent drops with no explanation.
  • Limits so strict normal use breaks.
  • Inconsistent behaviour across servers.

Rate limiting vs. related concepts

Newcomers often mix up rate limiting with a handful of neighbouring concepts. It is worth separating them clearly, because each solves a slightly different problem:

ConceptWhat it controlsAnalogy
Rate LimitingHow many requests per unit time a client can makeToll booth pacing cars per minute
ThrottlingThe mechanism that slows / rejects once a limit is hitThe toll gate arm coming down
Load SheddingDropping low-priority traffic when the whole system is overloaded, regardless of per-client limitsA restaurant turning away all new guests, not just noisy tables, when the kitchen is already overwhelmed
Circuit BreakingTemporarily stopping calls to a failing downstream dependencyA fuse box tripping to protect the wiring
BackpressureSignalling upstream producers to slow down when a consumer cannot keep upA conveyor belt operator waving “slow down, I cannot keep up”
QuotaA longer-term usage cap, often tied to billingA prepaid data plan for the month

Rate limiting is usually the first and cheapest of these tools to implement, and it often works alongside the others rather than replacing them. A well-built API might use rate limiting at the edge, load shedding under extreme stress, and circuit breakers between internal services — all at the same time, each catching a different failure mode.

Who gets rate limited?

It helps to remember that “the client” is not always a human typing into a browser. In practice, rate limiters have to think about several very different kinds of traffic sources:

  • End users browsing a website or using a mobile app — usually generate low, human-paced traffic.
  • Third-party developers building integrations against your public API — traffic patterns vary widely and can spike unpredictably.
  • Internal services calling each other inside a microservices architecture — often high-volume and very bursty.
  • Automated bots and crawlers — some legitimate (search engine indexers), some abusive (scrapers, credential-stuffing tools).
  • AI agents and scripts — an increasingly common category that can issue requests at machine speed, far faster than any human ever could.

04 · ARCHITECTURE & COMPONENTS

A Small Subsystem, Not An If-Statement

A production rate limiter is not just “an if-statement.” It is a small subsystem with several moving parts working together.

RATE-LIMITER PLACEMENT The limiter sits between the gateway and the app — every request pays a cheap check before any real work happens. Client app · bot · script API Gateway / Load Balancer Rate Limiter Shared Counter Store Redis · Memcached App Server Database business data 429 Too Many Retry-After header Monitoring & Metrics allowed vs denied · top offenders under limit over limit read / increment counter logs & metrics
Fig 1 · The limiter sits on the request path, backed by a shared counter store (Redis) and emitting telemetry — cheap to check, catastrophic to skip.

Key components

IX

Identifier extractor

Determines the “key” for a request — usually from an API key, JWT claim, or client IP.

CS

Counter store

Where request counts live. In-memory for single servers; a shared store like Redis for distributed systems.

AL

Limiting algorithm

The logic that decides “allow” or “deny” — token bucket, sliding window, and so on.

PE

Policy engine

Maps clients / tiers to specific limits (e.g., free tier vs. paid tier).

RH

Response handler

Builds the 429 response, including helpful headers like Retry-After.

TE

Telemetry

Logs and emits metrics so engineers can see limiter behaviour over time.

Where does it live?

Rate-limiting logic can be placed at several layers of the stack, and large systems often use more than one layer together:

LayerExampleNotes
Edge / CDNCloudflare, AkamaiBlocks abuse before it even reaches your infrastructure
API GatewayKong, AWS API Gateway, NGINXCentralised, language-agnostic, easy to configure per route
Application MiddlewareCustom Java / Spring filterFine-grained, business-aware (e.g., per-user tier logic)
Database / Service LayerConnection pool limitsLast line of defence against backend overload

Example policy configuration

In most gateway or middleware tools, a rate-limit policy is expressed declaratively rather than hard-coded. A simplified example might look like this:

JSON · DECLARATIVE PER-ROUTE POLICY
{
  "policies": [
    { "route": "/api/v1/search",     "tier": "free", "limit": 20,   "window": "1m", "burst": 5 },
    { "route": "/api/v1/search",     "tier": "pro",  "limit": 200,  "window": "1m", "burst": 40 },
    { "route": "/api/v1/auth/login", "tier": "*",    "limit": 5,    "window": "1m", "burst": 0 },
    { "route": "/api/v1/health",     "tier": "*",    "limit": 1000, "window": "1m", "burst": 200 }
  ]
}

Notice how the sensitive /auth/login route gets a tight limit with zero burst allowance regardless of tier, while the cheap /health route gets a very generous one. This kind of declarative configuration lets operations teams tune limits without redeploying application code — a big win for iterating safely in production.

05 · INTERNAL WORKING — THE ALGORITHMS

Five Classic Algorithms And When To Use Each

This is the heart of rate limiting. There are five classic algorithms, each with different trade-offs between accuracy, memory use, and burst tolerance.

5.1 Fixed window counter

Divide time into fixed blocks (e.g., every 00:00–00:59 of a minute). Count requests in the current block; reset to zero at the start of the next block.

Weakness: a client can send the full limit right at the end of one window and again right at the start of the next, doubling the effective rate for a short burst — this is called the boundary problem.

JAVA · FIXED WINDOW COUNTER
// Fixed Window Counter - simplified Java example
public class FixedWindowLimiter {
    private final int limit;
    private final long windowMillis;
    private long windowStart;
    private int count;

    public FixedWindowLimiter(int limit, long windowMillis) {
        this.limit = limit;
        this.windowMillis = windowMillis;
        this.windowStart = System.currentTimeMillis();
    }

    public synchronized boolean allowRequest() {
        long now = System.currentTimeMillis();
        if (now - windowStart >= windowMillis) {
            windowStart = now;   // start a brand new window
            count = 0;
        }
        if (count < limit) {
            count++;
            return true;
        }
        return false; // limit exceeded
    }
}

Fixed window: simple to implement, cheap on memory, but allows edge bursts.

5.2 Sliding window log

Store a timestamp for every request. To check a new request, remove timestamps older than the window and count what is left. Extremely accurate, but memory grows with request volume.

JAVA · SLIDING WINDOW LOG
// Sliding Window Log - simplified Java example using a deque
public class SlidingWindowLogLimiter {
    private final int limit;
    private final long windowMillis;
    private final Deque<Long> timestamps = new ArrayDeque<>();

    public SlidingWindowLogLimiter(int limit, long windowMillis) {
        this.limit = limit;
        this.windowMillis = windowMillis;
    }

    public synchronized boolean allowRequest() {
        long now = System.currentTimeMillis();
        while (!timestamps.isEmpty() && now - timestamps.peekFirst() > windowMillis) {
            timestamps.pollFirst(); // drop old timestamps outside the window
        }
        if (timestamps.size() < limit) {
            timestamps.addLast(now);
            return true;
        }
        return false;
    }
}

5.3 Sliding window counter (hybrid)

A practical compromise: combine the previous window’s count and the current window’s count, weighted by how far into the current window we are. This smooths the boundary problem without storing every timestamp.

PSEUDOCODE · WEIGHTED HYBRID
// Sliding Window Counter (weighted average of two fixed windows)
double estimatedCount = previousWindowCount * (1 - elapsedFractionOfCurrentWindow)
                      + currentWindowCount;
boolean allowed = estimatedCount < limit;

5.4 Token bucket

Picture a bucket that holds tokens. Tokens are added at a steady rate (e.g., 10 tokens/second) up to a maximum capacity. Each request consumes one token. If the bucket is empty, the request is rejected. This naturally allows bursts (if the bucket is full) while enforcing a long-term average rate.

TOKEN BUCKET ALGORITHM Steady refill rate + fixed capacity = allow bursts up to N, then throttle to the sustained rate. Token Generator adds tokens · fixed rate Bucket max capacity N tokens Incoming Request HTTP GET / POST Bucket has a token? Request Allowed take 1 token · forward Request Rejected HTTP 429 · Retry-After refill check arrives yes: −1 token no tokens
Fig 2 · Token bucket — the algorithm behind Stripe, AWS, and most modern public APIs. Steady average, controlled bursts.
JAVA · TOKEN BUCKET
// Token Bucket - simplified Java example
public class TokenBucketLimiter {
    private final long capacity;
    private final double refillTokensPerMillis;
    private double availableTokens;
    private long lastRefillTimestamp;

    public TokenBucketLimiter(long capacity, double refillTokensPerSecond) {
        this.capacity = capacity;
        this.refillTokensPerMillis = refillTokensPerSecond / 1000.0;
        this.availableTokens = capacity;
        this.lastRefillTimestamp = System.currentTimeMillis();
    }

    public synchronized boolean allowRequest() {
        refill();
        if (availableTokens >= 1) {
            availableTokens -= 1;
            return true;
        }
        return false;
    }

    private void refill() {
        long now = System.currentTimeMillis();
        double tokensToAdd = (now - lastRefillTimestamp) * refillTokensPerMillis;
        availableTokens = Math.min(capacity, availableTokens + tokensToAdd);
        lastRefillTimestamp = now;
    }
}

5.5 Leaky bucket

The mirror image of token bucket. Requests fill a bucket (queue); the bucket “leaks” (processes requests) at a constant rate, no matter how fast requests arrive. If the bucket overflows, new requests are dropped. This produces a perfectly smooth outgoing rate — useful when the downstream system truly cannot handle any bursts at all.

Token bucket

  • Allows healthy bursts.
  • Simple math, cheap memory.
  • Industry favourite (Stripe, AWS).

Leaky bucket

  • Smooths traffic perfectly but adds latency (queueing).
  • Bursts get delayed, not allowed.
AlgorithmMemoryBurst friendlyAccuracy
Fixed WindowVery lowYes (can double at edges)Low
Sliding LogHighNo hard edgesVery high
Sliding CounterLowMostly smoothHigh
Token BucketVery lowYes, controlledHigh
Leaky BucketMedium (queue)No — smooths outputHigh

5.6 A worked example: why the boundary problem matters

Let us make the “fixed window” weakness concrete with numbers. Say the limit is 100 requests per minute, using a fixed window that resets exactly on the clock minute.

  • At 00:59.9 (the very last moment of window 1), a client fires off 100 requests. All are allowed, because window 1’s count was 0 until that instant.
  • At 01:00.1 (a fraction of a second later, now inside window 2), the same client fires off another 100 requests. All are allowed again, because window 2’s count just reset to 0.

The result: the client sent 200 requests in roughly 0.2 seconds, despite a “100 requests per minute” rule technically never being violated in either individual window. This is exactly the kind of edge case that a sliding window counter or token bucket avoids, because they do not have a hard reset point where the count magically drops to zero.

5.7 Choosing an algorithm

PR

Prototyping / low traffic

Fixed window — dead simple, fast to build, fine when precision does not matter much yet.

PA

Public API with paying customers

Token bucket — predictable average rate with fair burst allowance, easy to explain in docs.

SD

Strict downstream systems

Leaky bucket — when the backend truly cannot tolerate any burst at all (e.g., a legacy system with a fixed processing rate).

HS

High-accuracy security use cases

Sliding window log or sliding window counter — when precise enforcement (like login-attempt limiting) matters more than memory savings.

06 · DATA FLOW & REQUEST LIFECYCLE

What Happens To One Request, End To End

Let us trace exactly what happens to a single request, end to end, when rate limiting is in place.

1

Request arrives

A client sends an HTTP request with an API key or auth token attached.

2

Identity extraction

The gateway pulls out the client’s identifying key (API key, user ID, or IP).

3

Policy lookup

The system checks which rate-limit tier applies to this key (e.g., “Free: 60/min”).

4

Counter check

The limiter algorithm checks the current count against the allowed limit, usually via a fast lookup in Redis or local memory.

5

Decision

If under the limit, the counter increments and the request proceeds. If over, it is rejected immediately.

6

Response

Allowed requests continue to the application. Rejected requests get a 429 with helpful headers.

7

Telemetry

The event (allowed / denied) is logged and metrics are updated for dashboards and alerts.

REQUEST LIFECYCLE (ALLOWED) Sequence of a single allowed request across client, gateway, counter store, and app server. Client API Gateway Rate Limiter Store App Server HTTP Request + API Key GET current count for key count = 42, limit = 50 INCR count forward request 200 OK 200 OK + rate-limit headers
Fig 3 · Sequence of an allowed request. Every hop pays a millisecond-scale limiter check before any expensive work runs.
Cheap check first, expensive work later

When a request is rejected, it should still be cheap. Checking the limit must happen before any expensive work (database queries, business logic) — otherwise the rate limiter is not protecting anything.

What a well-behaved client does on the other end

The lifecycle does not end at the server. A well-designed client library should treat a 429 response as useful information, not just an error to log and forget. The typical pattern is:

JAVA · CLIENT-SIDE EXPONENTIAL BACKOFF THAT RESPECTS RETRY-AFTER
// Simple exponential backoff retry - simplified Java example
public Response callWithBackoff(ApiClient client, Request request) throws InterruptedException {
    int maxRetries = 5;
    long baseDelayMillis = 500;

    for (int attempt = 0; attempt < maxRetries; attempt++) {
        Response response = client.execute(request);
        if (response.statusCode() != 429) {
            return response; // success or a non-retryable error
        }
        String retryAfter = response.header("Retry-After");
        long delay = retryAfter != null
            ? Long.parseLong(retryAfter) * 1000
            : baseDelayMillis * (1L << attempt); // exponential fallback: 500ms, 1s, 2s, 4s...
        Thread.sleep(delay);
    }
    throw new RuntimeException("Exceeded retry attempts due to rate limiting");
}

Respecting Retry-After when it is present, and falling back to exponential backoff with jitter when it is not, keeps well-behaved clients from making the problem worse by retrying too aggressively right after being rejected.

07 · ADVANTAGES, DISADVANTAGES & TRADE-OFFS

The Central Trade-Off: Accuracy Vs. Cost

Advantages

  • Protects backend resources from overload.
  • Improves fairness across clients.
  • Mitigates abuse, scraping, and brute-force attacks.
  • Enables predictable capacity planning and billing tiers.
  • Improves overall system stability and uptime.

Disadvantages

  • Adds latency (a lookup / check on every request).
  • Adds operational complexity (shared state, sync across servers).
  • Poorly tuned limits can frustrate legitimate users.
  • Distributed rate limiting has real consistency challenges.
  • Another moving part that can itself fail or become a bottleneck.

The key trade-off is almost always accuracy vs. cost. A perfectly accurate limiter (like sliding window log) uses more memory and CPU. A cheap limiter (like fixed window) is fast but slightly imprecise at the edges. Most production systems accept a small amount of imprecision in exchange for speed and simplicity — token bucket and sliding window counters are the most common “good enough” choices.

08 · PERFORMANCE & SCALABILITY

The Distributed Counting Problem

In a single-server setup, rate limiting is trivial: keep counters in local memory. The challenge appears once you have many servers behind a load balancer — because a client’s requests can land on any of them.

The distributed counting problem

If each server keeps its own local counter, a client could get 3× the allowed rate by hitting three different servers. The fix is a shared, centralised counter store — almost always Redis, because of its speed and atomic operations.

JAVA + LUA · DISTRIBUTED TOKEN BUCKET, ATOMIC IN REDIS
// Distributed Token Bucket using Redis (Lua script for atomicity)
// Executed atomically inside Redis via EVAL
String luaScript =
    "local tokens_key = KEYS[1] " +
    "local timestamp_key = KEYS[2] " +
    "local rate = tonumber(ARGV[1]) " +
    "local capacity = tonumber(ARGV[2]) " +
    "local now = tonumber(ARGV[3]) " +
    "local requested = tonumber(ARGV[4]) " +
    "local last_tokens = tonumber(redis.call('get', tokens_key) or capacity) " +
    "local last_refreshed = tonumber(redis.call('get', timestamp_key) or now) " +
    "local delta = math.max(0, now - last_refreshed) " +
    "local filled_tokens = math.min(capacity, last_tokens + (delta * rate)) " +
    "local allowed = filled_tokens >= requested " +
    "if allowed then filled_tokens = filled_tokens - requested end " +
    "redis.call('setex', tokens_key, 60, filled_tokens) " +
    "redis.call('setex', timestamp_key, 60, now) " +
    "return allowed";

Redis executes this atomically, so no matter which app server calls it, the count stays consistent across the whole fleet.

Scaling techniques

SH

Sharding by key

Route each client’s counter checks consistently to the same Redis shard using consistent hashing.

LG

Local + global hybrid

Give each server a small local budget, synced periodically to a global counter — trades perfect accuracy for lower latency.

AC

Approximate counting

Use probabilistic structures (like sliding window counters) instead of exact logs at very high scale.

ED

Edge rate limiting

Push simple limits to the CDN / edge layer so obviously abusive traffic never reaches your core infrastructure.

<1 mstypical Redis GET / INCR latency
O(1)token bucket check complexity
~100k+ops/sec a single Redis node handles
unshared local counters can leak the limit

Consistency vs. latency trade-off

This is really a small-scale version of the CAP theorem showing up inside a single feature. A perfectly consistent global counter means every single request, no matter which server or region it hits, sees the exact same count — but that requires a network round trip to a central store on every request, adding latency. Relaxing consistency (allowing servers to be slightly “out of sync” for a few milliseconds) buys speed at the cost of occasionally letting a client sneak a few extra requests through. In practice:

  • Strong consistency (every check hits the shared store synchronously) is common for security-sensitive limits, like login attempts, where being slightly generous is dangerous.
  • Eventual consistency (local counters synced periodically) is common for general API rate limits, where being off by a handful of requests is a non-issue.

Most real systems land somewhere in the middle: an accurate, low-latency local check backed by an asynchronous sync to a shared store, so the “true” global count only briefly lags reality rather than requiring a network call on every single request.

09 · HIGH AVAILABILITY & RELIABILITY

What Happens When The Limiter Itself Fails?

What happens if the rate limiter’s own storage (say, Redis) goes down? This is a real design decision, and there are two philosophies:

Fail-open vs. fail-closed

StrategyBehaviour on failureRisk
Fail-openAllow all requests through if the limiter is unreachableBackend could get overwhelmed during an outage
Fail-closedReject all requests if the limiter is unreachableTotal outage for legitimate users too

Most production systems choose fail-open with a fallback: use a local, less accurate in-memory limiter as a backup when the shared store is unavailable, rather than blocking everyone or letting everything through unchecked.

Making the limiter itself highly available

  • Redis replication + Sentinel / Cluster: avoids a single point of failure in the counter store.
  • Circuit breakers: if the counter store is slow or down, stop calling it and fall back gracefully instead of adding latency to every request.
  • Timeouts: rate-limit checks must have strict timeouts (a few milliseconds) — a slow limiter is worse than no limiter.
  • Redundant regions: for global APIs, run rate-limiting infrastructure per region to avoid cross-region network latency.
Rule of thumb

Your rate limiter should never become the reason your API goes down. It exists to protect availability, not threaten it.

Testing for reliability

Because the rate limiter sits on the critical path of every single request, it deserves the same reliability-testing rigour as any core piece of infrastructure:

CH

Chaos testing

Deliberately kill the counter store in a staging environment and confirm the fallback behaviour (fail-open or fail-closed) works as designed.

LT

Load testing

Simulate realistic peak traffic to confirm the limiter itself does not become the bottleneck under pressure.

CK

Clock skew testing

Verify windowed algorithms behave correctly even when server clocks drift slightly out of sync.

FD

Failover drills

Regularly practise failing over to a backup Redis replica to make sure the switch is fast and seamless.

10 · SECURITY CONSIDERATIONS

Defence-In-Depth, Not A Silver Bullet

Rate limiting is one of the most important security controls an API can have, but it must be implemented carefully to actually be effective.

Common attacks it defends against

BF

Brute-force login

Limits how many password guesses an attacker can attempt per minute.

CS

Credential stuffing

Slows down attackers testing large lists of stolen username / password pairs.

DO

Denial of service

Prevents a single source from consuming all server capacity.

SC

Data scraping

Slows automated bots that try to extract your entire dataset via the API.

Common pitfalls

  • Limiting by IP alone: attackers can rotate IPs or hide behind shared corporate NATs. Combine IP-based limits with account-based limits.
  • Trusting client-supplied headers: never trust an X-Forwarded-For header blindly without validating it comes from a trusted proxy.
  • Leaking sensitive info in error messages: a 429 response should tell the client to slow down, not reveal internal system details.
  • Not rate limiting expensive endpoints harder: a “search” or “export” endpoint that hits the database heavily deserves a stricter limit than a simple health check.
Layered defence only

Rate limiting is a defence-in-depth measure, not a silver bullet. It should be combined with authentication, input validation, and Web Application Firewalls (WAFs) — not used as the only line of defence.

Adaptive and behaviour-based limiting

Static limits (“100/minute for everyone”) are a good starting point, but sophisticated attackers learn to stay just under the threshold. More advanced systems use adaptive rate limiting, which adjusts limits dynamically based on observed behaviour rather than a single fixed number:

  • Reputation scoring: clients with a long history of well-behaved traffic get more headroom; new or previously-flagged clients get tighter limits.
  • Anomaly detection: a sudden, sharp deviation from a client’s normal traffic pattern (even if still technically under the raw limit) can trigger extra scrutiny or a temporary tighter cap.
  • Progressive penalties: repeatedly hitting the limit can escalate consequences — first a short cooldown, then a longer one, then a temporary account flag for manual review.

These techniques add complexity and are usually reserved for high-value endpoints (like login, password reset, and payment) where the cost of a successful attack is much higher than the cost of the extra engineering.

11 · MONITORING, LOGGING & METRICS

You Cannot Tune What You Cannot See

You cannot tune what you cannot see. A good rate-limiting system emits rich telemetry so engineers can adjust limits based on real traffic patterns.

Key metrics to track

MetricWhy it matters
Requests allowed vs. denied (per key / route)Shows how close clients are to their limits
429 response rateSpikes may mean limits are too strict, or an attack is happening
Limiter check latencyThe limiter itself must stay fast (sub-millisecond ideally)
Top offendersIdentifies which clients / IPs are hitting limits most often
Counter store healthRedis / Memcached CPU, memory, replication lag

Useful response headers

HTTP · A GOOD 429 RESPONSE
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1737300000
Retry-After: 42
Content-Type: application/json

{
  "error": "rate_limit_exceeded",
  "message": "You have exceeded 100 requests per minute. Try again in 42 seconds."
}

Exposing X-RateLimit-Remaining and Retry-After lets well-behaved clients self-throttle automatically, reducing repeated failed attempts.

The earliest signal of trouble

Set up alerts on sudden jumps in 429 rates — this is often the earliest signal of either an abusive client or a misconfigured internal service retrying too aggressively.

Building a rate-limit dashboard

A useful operational dashboard for rate limiting typically includes a handful of views, each answering a different question an on-call engineer might ask at 3am:

Q1

“Is anything broken right now?”

A real-time graph of overall 429 rate as a percentage of total traffic, with an alert threshold.

Q2

“Who is being throttled?”

A top-N table of clients / keys generating the most 429s in the last hour.

Q3

“Is the limiter itself healthy?”

Latency and error-rate graphs for calls to the counter store (e.g., Redis).

Q4

“Are our limits well-tuned?”

A distribution showing what percentage of clients typically use what fraction of their limit — helps spot limits set far too high or too low.

Logging every denied request with enough context (key, route, timestamp, and which limit was breached) also makes it possible to reconstruct exactly what happened during an incident after the fact, without needing to reproduce it live.

12 · DEPLOYMENT & CLOUD

You Rarely Build A Limiter From Scratch

Most teams do not build rate limiting completely from scratch — they use a mix of managed services and lightweight custom code.

GW

API gateways

Kong, Apigee, AWS API Gateway, and Azure API Management all have built-in rate-limiting policies configurable per route or per key.

CD

CDN / edge

Cloudflare and Akamai can rate-limit at the network edge, blocking abusive traffic before it ever reaches your servers.

SM

Service mesh

Istio and Envoy support rate limiting as a sidecar policy for microservices, independent of application code.

CM

Custom middleware

Frameworks like Spring (Java), Express (Node), and Django (Python) all have rate-limiting libraries / filters you can drop in.

Multi-region deployment

For globally distributed APIs, a single global counter (e.g., one Redis cluster) can add latency for far-away regions. Two common approaches:

  • Regional limits: each region enforces its own independent limit (simpler, but a client could theoretically get N× the limit by spreading across N regions).
  • Globally synced counters: counters replicate across regions with eventual consistency — more accurate, more complex, slightly higher latency.

Build vs. buy

Teams frequently ask whether to build a custom rate limiter or rely on an off-the-shelf tool. There is no universally correct answer, but a few guidelines help:

SituationRecommendation
Small team, standard REST APIUse your API gateway’s built-in rate limiting — it is already tested and battle-hardened
Complex, business-specific tiers (per-plan, per-feature)Build custom middleware on top of a shared Redis store
Massive scale, global footprintCombine edge-layer limiting (CDN) with a custom, sharded internal solution
Microservices with many internal callsUse a service mesh (Istio / Envoy) so limiting is consistent without touching every service’s code

Building a rate limiter from scratch is a great learning exercise (and this guide gives you everything needed to do it), but in production, most teams are better served by configuring proven, existing tools and reserving custom code for the parts that are genuinely unique to their business.

13 · DATABASES, CACHING & LOAD BALANCING

The Counter Store Is A Very Specialised Cache

Rate limiting sits right at the intersection of caching and load balancing — the counter store is essentially a very specialised cache.

Why Redis is the default choice

  • In-memory speed: counter checks need to complete in microseconds, not the milliseconds a disk-backed database would take.
  • Atomic operations: commands like INCR and Lua scripting let you safely update counts even with thousands of concurrent requests.
  • Built-in expiry (TTL): a counter key can automatically disappear after the window ends — no manual cleanup needed.
JAVA · FIXED WINDOW USING REDIS INCR + EXPIRE
// Simple fixed-window rate limit using Redis INCR + EXPIRE
public boolean isAllowed(String clientKey, int limit, int windowSeconds, Jedis redis) {
    String redisKey = "rl:" + clientKey;
    long current = redis.incr(redisKey);
    if (current == 1) {
        redis.expire(redisKey, windowSeconds); // set TTL only on first request
    }
    return current <= limit;
}

Interaction with load balancers

Load balancers distribute traffic across servers, which is exactly why local, per-server counting fails in distributed setups (as covered in Section 8). Some load balancers (like NGINX and HAProxy) even have their own basic rate-limiting modules that can act as a first coarse filter before requests reach the application layer, reducing load on the shared Redis store.

Layer your defences

A coarse limit at the load balancer catches obvious floods cheaply, while a precise limit at the application layer (backed by Redis) enforces accurate per-user policies.

Cost of the counter store itself

It is easy to forget that the rate limiter’s own storage has real operational costs. Every counter key consumes memory, and at very high scale (millions of unique clients), this adds up. A few practical tips keep this manageable: set a TTL slightly longer than the window so stale keys expire automatically, avoid storing per-request timestamps at massive scale (favour token bucket or sliding window counters over sliding window logs), and periodically review whether inactive or churned clients’ keys are being cleaned up rather than accumulating forever.

14 · APIS, MICROSERVICES & RATE LIMITING

Different Clients, Different Rules

In a microservices architecture, rate limiting gets more nuanced because there are multiple kinds of “clients” to think about.

External vs. internal rate limiting

TypePurposeTypical enforcement point
External (client-facing)Protect the platform from external users / appsAPI Gateway / Edge
Internal (service-to-service)Prevent one microservice from overwhelming anotherService mesh (Envoy / Istio) or client libraries
EXTERNAL + INTERNAL LIMITING Public traffic is throttled at the edge; service-to-service traffic is throttled by the mesh or client library. External Client web / mobile / partner API Gateway external rate limit Order Service orchestrator Inventory Service internal RL: mesh policy Payment Service internal RL: client lib limited at edge allowed internal limit internal limit
Fig 4 · External vs. internal — the gateway protects the platform; the mesh protects services from each other.

GraphQL is different

Unlike REST, where one endpoint usually equals one unit of work, a single GraphQL query can request a huge, deeply nested amount of data. Simple “requests per minute” limiting is not enough — GraphQL APIs often use query complexity / cost analysis, assigning a “cost” to each field and rate-limiting based on total cost per window rather than raw request count.

Batch and bulk APIs

Some APIs let a client bundle many operations into a single HTTP call (a “batch” endpoint). This creates an interesting question: does one batch request count as “1” against the rate limit, or as “N” (the number of operations inside it)? Most well-designed systems count batch requests by their effective work, not their HTTP request count — otherwise clients are incentivised to batch everything into one giant call purely to dodge the limiter, which defeats its purpose of protecting real backend capacity.

Webhooks and outbound rate limiting

Rate limiting is not only about incoming requests. When a platform sends outbound webhooks to a customer’s server, it also needs to rate-limit itself — otherwise a burst of events (say, 10,000 orders placed in one second during a flash sale) could overwhelm the receiving server. This is typically handled with a queue plus a leaky-bucket-style outbound sender, so events are delivered at a steady, sustainable pace rather than all at once.

15 · DESIGN PATTERNS & ANTI-PATTERNS

The Patterns That Age Well

Good patterns

TL

Tiered limits

Different limits for free, pro, and enterprise plans — ties rate limiting to your business model.

GD

Graceful degradation

Instead of hard-rejecting, some systems slow down or serve cached / stale data when near the limit.

EB

Exponential backoff (client-side)

Well-behaved clients should retry with increasing delays after a 429, rather than retrying immediately.

PE

Per-endpoint limits

Expensive endpoints (search, export, bulk-write) get stricter limits than cheap ones (health checks).

Anti-patterns to avoid

  • One global limit for everything: treats a cheap GET /status call the same as an expensive POST /report call.
  • No feedback to the client: returning a bare 429 with no headers or explanation forces clients to guess when to retry.
  • Synchronous, blocking limiter calls with no timeout: if your counter store hangs, every request hangs with it.
  • Limiting only by IP in a NAT / proxy-heavy world: punishes entire offices or countries behind shared IPs.
  • Changing limits without notice: breaks integrations that were built against documented limits.
“A rate limit that nobody can predict is indistinguishable from a random outage.”

16 · BEST PRACTICES & COMMON MISTAKES

A Portable Checklist

Best practices

  • Document your limits clearly in the API docs.
  • Return Retry-After and X-RateLimit-* headers on every response.
  • Use token bucket or sliding window counter for most use cases.
  • Set stricter limits on expensive / sensitive endpoints.
  • Fail open with a safe fallback if the counter store is down.
  • Monitor 429 rates and top offenders continuously.
  • Version your rate-limit policy so changes are traceable.

Common mistakes

  • Setting limits based on guesses instead of real traffic data.
  • Forgetting to rate-limit authentication endpoints.
  • Not testing behaviour under the shared-store outage scenario.
  • Applying the same limit to bots, mobile apps, and internal services alike.
  • Silently changing limits without informing API consumers.

Rolling out rate limiting without breaking existing users

One of the trickiest real-world challenges is not designing the algorithm — it is introducing rate limiting to an API that has been running without it for years, where some existing integrations may already be sending far more traffic than any “reasonable” limit would allow. A safe rollout typically follows these stages:

1

Observe only

Deploy the limiter in “dry run” mode — it calculates whether a request would have been blocked, logs it, but still lets everything through. This reveals real traffic patterns without breaking anyone.

2

Set generous limits

Based on the observed data, set initial limits well above typical usage (e.g., 2–3× the 99th percentile) so only truly abnormal traffic is affected.

3

Warn before you block

Add response headers showing remaining quota, and consider emailing top consumers who are close to breaching limits, before enforcement actually begins.

4

Enforce gradually

Turn on real blocking for a small percentage of traffic or a subset of endpoints first, monitor closely, then expand.

5

Tighten over time

Once the system is stable and consumers have adapted, gradually reduce limits toward the values that best protect the platform long-term.

Rate limits are a public contract

Treat your rate limits like a public API contract, not an internal implementation detail — changing them abruptly is effectively a breaking change for every integration built against the old numbers.

17 · REAL-WORLD EXAMPLES

How The Big Platforms Actually Do It

GH

GitHub

Uses a token-bucket-like model: authenticated REST API requests are limited to 5,000/hour, with clear X-RateLimit-* headers on every response and a separate, more flexible GraphQL cost-based limit.

ST

Stripe

Uses a token bucket approach with a “sustained rate” plus a “burst” allowance, so brief spikes during checkout do not fail, but sustained abuse is throttled.

TW

Twitter / X

Applies per-endpoint limits (different limits for reading tweets vs. posting them) with 15-minute windows, historically one of the most well-documented public rate-limit systems.

SH

Shopify

Uses a “leaky bucket” for its REST Admin API — a bucket size (burst) plus a steady leak (restore) rate, explicitly named as such in their docs.

NF

Netflix

Internally uses rate limiting between microservices (via tools like Hystrix’s successors and service-mesh policies) to prevent cascading failures when one service slows down.

GA

Google Cloud / AWS

Both offer managed API Gateway rate limiting plus quota systems combining short-term rate limits with long-term monthly quotas per project / API key.

5,000/hrGitHub authenticated REST default
Token BucketStripe’s core algorithm
15 minclassic Twitter window size
LeakyShopify Admin REST bucket

18 · FREQUENTLY ASKED QUESTIONS

Short Answers To The Questions That Come Up Most

Is rate limiting the same as throttling?

They are closely related. Rate limiting is the overall policy (“100 requests/minute”); throttling is the mechanism that actually slows or rejects requests once that policy is triggered.

What HTTP status code should be returned when a limit is hit?

429 Too Many Requests is the standard status code, typically paired with a Retry-After header telling the client how long to wait.

Should I rate limit by IP or by API key?

Both, ideally. API keys / user IDs identify a specific client reliably; IP-based limits provide a backstop against unauthenticated abuse (like login brute-forcing) where no key exists yet.

Which algorithm should I use by default?

Token bucket or sliding window counter are the most common defaults — they balance accuracy, burst tolerance, and low memory cost well for most APIs.

Does rate limiting replace authentication and authorisation?

No. Rate limiting controls how much a client can do; authentication / authorisation controls what a client is allowed to do. Both are needed together.

Can rate limiting fully stop a DDoS attack?

Not alone. It helps a lot against smaller-scale abuse and single-source floods, but large distributed attacks usually need dedicated DDoS protection (like a CDN / edge scrubbing service) in front of the API.

How do I choose the right limit numbers?

Start with real traffic data from normal usage, add headroom for legitimate bursts, and adjust based on monitoring — never guess blindly, and always leave room to tune limits without a full redeploy.

Should rate limits differ per endpoint?

Yes, almost always. A cheap health-check endpoint can tolerate a much higher rate than a heavy search, export, or write-heavy endpoint that touches the database extensively.

What is the difference between rate limiting and a quota?

A rate limit controls short-term pace (e.g., requests per second or minute) to smooth out load, while a quota tracks a longer-term total (e.g., calls per month) usually tied to a pricing plan. Systems often enforce both together.

Can rate limiting be applied at the database level too?

Yes — connection pool limits and query concurrency caps act as a rate limiter of last resort, protecting the database even if something upstream fails to catch excess traffic first.

Is it better to reject requests or queue them?

It depends on the use case. Rejecting (as with token bucket) gives clients fast, clear feedback. Queueing (as with leaky bucket) smooths bursts but adds latency and risks timeouts if the queue grows too large — choose based on how tolerant your clients are of delay versus outright rejection.

19 · SUMMARY & KEY TAKEAWAYS

The Bakery Sign, Grown Up

API rate limiting is a foundational piece of building reliable, fair, and secure APIs. It protects backend resources, keeps costs predictable, defends against abuse, and ensures that no single client can degrade service for everyone else — much like that bakery’s simple sign protecting its one oven for every customer.

Key takeaways

  • Rate limiting caps how many requests a client can make in a given time window, rejecting excess requests with a 429 response.
  • Five core algorithms exist — fixed window, sliding window log, sliding window counter, token bucket, and leaky bucket — each trading off accuracy, burst tolerance, and memory cost differently.
  • Token bucket is the most widely used default because it allows healthy bursts while enforcing a steady average rate.
  • In distributed systems, counters must live in a shared store like Redis so limits are enforced consistently across all servers.
  • Always design for failure — decide explicitly whether your system fails open or closed if the limiter itself goes down.
  • Good limiters communicate clearly with clients via headers (X-RateLimit-*, Retry-After), not silent drops.
  • Rate limiting is defence-in-depth, not a full security solution — pair it with authentication, validation, and edge protections.
  • Real companies like GitHub, Stripe, Shopify, and Twitter all publish and rely on rate limiting as a core, documented part of their public APIs.

Leave a Reply

Your email address will not be published. Required fields are marked *