Rate limiting

Rate-limiting algorithms compared: token bucket, leaky bucket, sliding window, and fixed window

Rate limiting constrains how often an identity can act in a time interval. Token bucket allows controlled bursts; leaky bucket shapes output at a steady rate; sliding window enforces recent-history fairness; fixed window is simple but can admit twice the limit at a boundary. Pick the algorithm for the resource you protect, key it on a stable identifier, and return 429 with retry information.

20 min read
In short: Rate limiting constrains how often an identity can act in a time interval. Token bucket allows controlled bursts; leaky bucket shapes output at a steady rate; sliding window enforces recent-history fairness; fixed window is simple but can admit twice the limit at a boundary. Pick the algorithm for the resource you protect, key it on a stable identifier, and return 429 with retry information.

Rate limiting is a control that constrains how frequently an identity – an IP address, API key, user, tenant, or service – can perform an action in a defined time interval. Every limiter records usage, measures time, and rejects work when capacity is exhausted. The algorithm that you choose decides fairness, burst behavior, and how much state you store.

Choosing the wrong algorithm can create boundary exploits, uneven load, or distributed consistency failures. This guide explains how token bucket, sliding window, fixed window, and leaky bucket work, where each fails, and how to choose one for an API.

The token bucket algorithm is a rate-limiting method that represents capacity as tokens that accumulate over time up to a configurable maximum. A request is allowed when it can spend the tokens it costs, and rejected when too few remain.

A token bucket rate limit typically:

  • Starts with a full bucket, which is the burst the system can absorb.
  • Refills tokens at a fixed rate, which is the sustained throughput it can afford.
  • Rejects a request that costs more tokens than remain.
  • Caps unused credit at capacity, so idle time cannot bank unlimited burst.

Rate limiting algorithms at a glance

AlgorithmFairnessBurst handlingMemoryBest for
Fixed windowLowUp to 2× at window boundariesOne counterSimple internal quotas
Sliding window logHighLowOne timestamp per requestStrict fairness enforcement
Sliding window counterMedium to highLow to mediumTwo countersScalable public APIs
Token bucketMedium to highUp to the configured capacityToken count and last refillDeveloper-facing APIs
Leaky bucketHigh output smoothingNone in the queue-based modelQueue depth or water levelTraffic shaping

These trade-offs lead to a few practical conclusions:

  • Fixed window is simple but allows boundary burst amplification of up to twice the configured limit.
  • Sliding log is exact, but memory grows with request volume.
  • Sliding counter is a practical compromise for scale: two integers instead of one timestamp per event.
  • Token bucket is a useful starting point when an API needs to allow controlled bursts.
  • Leaky bucket is useful for shaping outbound or downstream traffic.

As a mental model, token bucket represents capacity accumulation, sliding window represents recent history, and fixed window represents discrete accounting periods.

What is rate limiting?

Rate limiting constrains how frequently an identity can perform an action within a defined time interval. The identity can be an IP address, API key, user ID, tenant ID, or service.

At a systems level, every rate limiter defines three things:

  • How time is measured.
  • How usage is recorded.
  • What happens when capacity is exhausted.

Rate limiting encodes a policy for fairness, abuse resistance, and infrastructure protection. The algorithm is how that policy is counted. The identifier is who shares the budget. A correct algorithm with a spoofable identifier is still a weak control.

What is the fixed window algorithm?

The fixed window algorithm divides time into discrete intervals such as 60 seconds. For each identity, a counter tracks how many requests occur within the current interval. When the counter exceeds the configured limit, the system rejects additional requests until the next window begins.

State is one integer plus a window identifier: typically a few tens of bytes per identity. An increment is O(1). In Redis this is INCR plus EXPIRE on a single key, which is why fixed window is inexpensive to run at high cardinality.

How the fixed window algorithm works

A limit of 100 requests per minute works as follows:

  • Requests between 12:00:00 and 12:00:59 increment one counter.
  • At 12:01:00, the counter resets to zero.

That reset is the failure mode. A client can send 100 requests at 12:00:59 and another 100 at 12:01:00. The system technically enforces 100 requests per minute, but 200 requests arrive within about one round-trip of the boundary. The amplification factor is 2× the configured max, independent of window length: a 10-request-per-hour quota has the same 2× spike.

When the fixed window algorithm fails

Boundary bursts cause problems in practice:

  • An attacker can brute-force a login endpoint at window boundaries, doubling the guesses the quota appears to allow.
  • Public APIs can experience load spikes at reset times, especially when many clients share a clock-aligned retry.
  • Multi-tenant systems can see unfair traffic distribution near window edges: one tenant exhausts the window early and waits, while another concentrates its entire quota into the last second.

Fixed window is appropriate when implementation simplicity matters and a 2× spike can't overload the protected resource. Avoid it on login, one-time passwords, checkout, or any path where that spike is itself the abuse.

What is the sliding window algorithm?

The sliding window algorithm evaluates requests against a rolling time interval instead of discrete blocks. At any moment it asks: how many events occurred in the last n seconds? The two primary implementations are sliding window log and sliding window counter.

How the sliding window log works

The system stores a timestamp for every request. When a new request arrives, the system removes timestamps older than the window duration, then counts the remaining entries. At any moment, the system evaluates exactly the last n seconds of traffic, which provides strong fairness. A 100-request-per-60-second log doesn't admit a 2× boundary spike, because the lookback is continuous.

Memory usage grows with request volume. Each event is one timestamp. In a compact in-process structure that is 8 bytes plus allocator overhead; in a Redis sorted set it is typically tens of bytes per member after skiplist overhead. A single identity sending 10,000 requests in a 60-second window therefore stores 10,000 timestamps. An attack that multiplies that client's volume multiplies the state for that key in the same proportion. During a traffic spike, pruning expired timestamps also costs CPU: O(k) for a list, or O(log n) per change in a sorted set (ZADD, ZREMRANGEBYSCORE, ZCARD).

Use a sliding log when exact recent-history fairness matters more than memory, and the expected events per window stay bounded. Don't use it as the only limiter on an unauthenticated public path that an attacker can inflate.

How the sliding window counter works

Instead of storing every timestamp, a sliding window counter blends counters from the current and previous fixed sub-window based on elapsed time. The usual estimate, described in Cloudflare's counting overview, is:

estimated = current + previous × (1 − elapsed / window)

At a window boundary the previous window still has full weight, so a client can't dump a full new quota on top of a full old one. Mid-window, the previous count fades linearly. State is two integers and a window start: on the order of 24 bytes per identity, regardless of whether that identity sent 10 or 10,000 requests. Relative to a log of 10,000 timestamps, that is roughly two to three orders of magnitude less state.

Accuracy is approximate. The interpolation assumes the previous window's traffic was uniformly distributed, so a client that burst at the end of the previous window is slightly over-counted, and a client that was idle then is slightly under-counted. For large-scale APIs, that error is usually acceptable. For a security control that must be exact, use a log.

What is the token bucket algorithm?

The token bucket algorithm represents capacity as tokens that accumulate over time up to a configurable maximum. A client that consumes tokens faster than the refill rate is rejected, while idle clients build up burst credit – which suits APIs that handle bursty request patterns.

Each identity has three properties:

  • A maximum bucket capacity.
  • A refill rate (tokens added per interval).
  • A current token count, plus the time of the last refill.

Requests consume tokens. If fewer tokens remain than the request costs, the system rejects it. If a client is idle, then tokens accumulate up to capacity. State is a token count and a timestamp: similar to fixed window, typically a few tens of bytes, and the update is O(1).

How the token bucket algorithm works

A bucket with capacity 20, refill rate 10, and interval 60 seconds admits a burst of 20 requests from a full bucket, then 10 more each minute. A client that spaces work evenly can sustain 10 requests per minute indefinitely. A client that idles for two minutes still only holds 20 tokens, not 30: capacity is a hard cap on credit.

The same numbers with a variable cost change the policy without changing the algorithm. Charge 1 token for a read, 5 for a write, and 50 for an export, and an expensive operation consumes burst credit that cheap operations would have used over several minutes. That is why token bucket is a usual choice for developer-facing APIs whose clients send batches and whose operations aren't equal.

Where the token bucket algorithm fails

Token bucket is a poor fit in two situations:

  • Extremely strict fairness requirements where recent history must be exact. A full bucket after idle time is a permitted burst, not a fairness bug, but it is the wrong behavior if you needed "no more than n in the last 60 seconds" with no credit.
  • Output that must be perfectly smoothed rather than burst-tolerant. A token bucket will release a burst as fast as the client can send; it doesn't pace outbound work.

It also fails when capacity is set equal to the refill without considering burst. Capacity 100 and refill 100 per minute is a burst of 100, which on a cold start looks like a fixed-window dump. Set capacity to the burst you can actually absorb, and refill to the sustained rate you can afford.

What is the leaky bucket algorithm?

The queue-based leaky-bucket model enforces a constant output rate. Incoming requests enter a queue that drains at a steady pace. If the queue is full, then the system rejects new requests. Unlike token bucket, this model doesn't accumulate burst capacity; it smooths traffic strictly.

If the leak is 50 requests per second and the queue holds 200, a full queue represents 4 seconds of backlog. Memory scales with queued work, not with historical volume: each waiting item is the request (or a reference to it) until it drains or is dropped. That's the opposite of a sliding log, which stores history, and the opposite of a token bucket, which stores credit.

Some systems use "leaky bucket" for a meter-based variant that tracks a water level rather than a queue. That variant has different burst behavior and is often implemented as a token bucket with a small capacity. Verify the implementation rather than relying on the name alone.

The leaky bucket model is useful for three purposes:

  • Protecting downstream services that can't absorb bursts.
  • Shaping outbound traffic to a partner quota.
  • Preventing retry storms from overwhelming dependencies.

Don't use a queue-based leaky bucket as a user-facing fairness control unless you're prepared to hold requests in memory and return them later. Most HTTP APIs reject immediately with 429 instead of queueing.

How does leaky bucket differ from token bucket?

The token bucket algorithm is a rate-limiting method that accumulates burst credit. The leaky bucket algorithm is a rate-limiting method that emits work at a steady rate. They are not interchangeable names for the same control.

PropertyToken bucketLeaky bucket
Burst handlingAllows a configured burst, then sustained refillQueue-based model emits at a constant rate; no burst credit
MemoryToken count and last refill: tens of bytes per identityQueue depth or water level; the queue variant stores waiting work
Clock dependencyRefill is computed from elapsed time since the last update

Drain rate is continuous and less sensitive to wall-clock alignment

Typical useDeveloper-facing APIs with bursty clients and variable costsTraffic shaping and protecting burst-intolerant downstreams

Choose token bucket when clients may burst and you can absorb that burst. Choose leaky bucket when a downstream quota or dependency must see a smooth arrival rate.

Distributed rate limiting and global coordination

Distributed rate limiting introduces coordination complexity. If multiple instances serve traffic for the same identity, they must share state. Without coordination, each node can independently allow traffic, multiplying the effective limit by the replica count. Five uncoordinated workers with a limit of 100 yield an effective limit of 500.

Strong versus eventual consistency

Strong consistency keeps limits accurate but increases latency and reduces availability: every decision waits on the shared store. A same-availability-zone Redis increment is typically about a millisecond. A cross-region coordinated check adds the inter-region round-trip, often 50 ms to 150 ms, to every request that must observe a global budget.

Eventual consistency improves resilience but allows temporary overages. In multi-region deployments, a partition lets each side of the split allow the full limit independently: two regions can double-allow traffic until they reconnect. That isn't an algorithm bug. It's the CAP trade-off of a shared counter.

Local in-process counters decide in microseconds and need no network, which is why they look attractive and why they fail the moment you scale horizontally. Prefer a shared store for any limit that must hold across instances. Prefer a store-side clock (for example Redis TIME) over each node's wall clock so a 500 ms VM clock skew doesn't shift sliding-window evaluation.

Common failure modes

Distributed limiters fail in a few recurring ways:

  • Redis hot keys form when many requests share one characteristic, or when the characteristic is missing and every client collapses onto a single key.
  • Network latency increases enforcement delays and makes a remote check a new timeout path.
  • Clock skew reduces the accuracy of sliding windows when each node timestamps events locally.
  • Region isolation allows a temporary limit bypass of N × the configured max for N isolated regions.

Distributed rate limiting is fundamentally a consistency problem. Algorithm choice matters, but state coordination dominates complexity at scale.

Rate limiting in microservices architectures

In microservices architectures, place rate limits deliberately. Limits at the edge or API gateway protect external exposure and enforce per-identity fairness. Internal limits serve a different purpose: they protect downstream dependencies and reduce the risk of cascading failures.

Problems arise when retries aren't coordinated with enforcement. If a downstream service begins failing and upstream services retry aggressively, total traffic can exceed the original request volume. Three retries with no jitter on a 100-request admission path can present 400 attempts to the dependency. In fan-out systems, where one request triggers multiple internal calls, this effect multiplies by the fan-out factor as well.

Effective rate limiting in microservices requires aligning enforcement with retry behavior and service topology. Apply a leaky bucket or concurrency cap on the outbound side of a dependency, and a token bucket or sliding window on the inbound identity. Otherwise the limiter can amplify instability instead of containing it.

Choose a rate limiting algorithm for APIs

For many public APIs, the token bucket algorithm is a useful starting point. It enforces a predictable long-term rate while allowing controlled bursts, which makes it well suited to developer-facing platforms whose clients send requests in batches and whose operations have different costs.

If strict fairness is the primary requirement, particularly in security-sensitive environments, a sliding window log evaluates exact recent request history. That precision comes with higher memory and operational cost: state grows with events, not with identities.

For large-scale distributed systems where memory usage and coordination overhead matter, a sliding window counter is often a practical compromise. It reduces boundary burst effects while remaining two integers per identity, which is efficient enough to operate at high throughput and high cardinality.

Fixed window is suitable when simplicity matters and a 2× boundary burst can't exhaust the protected resource. Leaky bucket is better suited to traffic shaping and steady output control than to user-facing API fairness.

Choose token bucket for controlled bursts and sliding window for recent-history fairness. Choose fixed window for simple accounting where boundary bursts are safe, and leaky bucket where downstream output must remain steady.

Choose the right identifier

The identifier determines who shares a limit. IP addresses work before authentication, but carrier networks and offices can place many users behind one address. Attackers can also rotate proxies. For authenticated APIs, add limits for user, account, API key, or tenant identifiers.

Apply more than one limit when risks differ. A service might enforce a generous tenant-wide quota, a smaller per-user limit, and a strict per-IP limit for failed authentication. Don't use raw email addresses or other personal data as identifiers when an opaque internal ID is available.

Trust client IP headers only when a known proxy added them. Otherwise, an attacker can spoof a new address on every request and treat the limiter as a per-request reset.

For more information about combining limits with other controls, see API security best practices. Identity-aware limits also help defend against distributed API abuse.

Expressing algorithms in application code

These algorithms are independent of language. Libraries expose them as named constructors so the policy lives next to the handler that enforces it. The following snippets show the same token-bucket policy – capacity 20, refill 10 tokens per 60 seconds, keyed on userId – in the Arcjet JavaScript, Python, and Go SDKs. Sliding window and fixed window use the same client with a different constructor. Port the shape; check each language's option names.

The following JavaScript example configures that policy:

import arcjet, { tokenBucket } from "@arcjet/node";
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
tokenBucket({
mode: "LIVE",
characteristics: ["userId"],
refillRate: 10,
interval: 60,
capacity: 20,
}),
],
});
const decision = await aj.protect(req, { userId, requested: 1 });
if (decision.isDenied() && decision.reason.isRateLimit()) {
// Return 429. decision.reason.reset is seconds until refill.
}

The following Python example configures the same policy:

import os
from arcjet import Mode, arcjet, token_bucket
aj = arcjet(
key=os.environ["ARCJET_KEY"],
rules=[
token_bucket(
mode=Mode.LIVE,
characteristics=["userId"],
refill_rate=10,
interval=60,
capacity=20,
),
],
)
async def handler(request, user_id):
decision = await aj.protect(
request,
requested=1,
characteristics={"userId": user_id},
)
if decision.is_denied() and decision.reason_v2.type == "RATE_LIMIT":
pass # Return 429

The following Go example configures the same policy:

aj, err := arcjet.NewClient(arcjet.Config{
Key: os.Getenv("ARCJET_KEY"),
Rules: []arcjet.Rule{
arcjet.TokenBucket(arcjet.TokenBucketOptions{
Mode: arcjet.ModeLive,
Characteristics: []string{"userId"},
RefillRate: 10,
Interval: time.Minute,
Capacity: 20,
}),
},
})
if err != nil {
return err
}
decision, err := aj.Protect(
r.Context(),
r,
arcjet.WithCharacteristics(map[string]string{"userId": userID}),
// WithRequested is token-bucket only. Sliding and fixed windows count events.
arcjet.WithRequested(1),
)
if err != nil {
// Fail-open: log and continue, or fail closed on sensitive routes.
} else if decision.IsDenied() && decision.Reason.IsRateLimit() {
// Return 429
}

requested is how token bucket expresses variable cost: pass 1 for a cheap read and a larger integer for an export. Sliding window and fixed window count events, so they have no equivalent field. In JavaScript those constructors are slidingWindow and fixedWindow; in Python, sliding_window and fixed_window; in Go, arcjet.SlidingWindow and arcjet.FixedWindow. For more information about options, see the rate limiting reference.

Any shared implementation still has to pick a store, an identifier, and a consistency model. The constructor names the algorithm; it doesn't remove those choices.

Return useful 429 responses

When a request exceeds an enforced limit, return the HTTP status code 429 Too Many Requests, as defined by RFC 6585. Keep the response body machine-readable and consistent. Where practical, use Retry-After to tell the client when it can retry. The proposed RateLimit and RateLimit-Policy fields can communicate current capacity and quota policy, but they remain an IETF Internet-Draft and might change before publication as an RFC.

Prefer exponential backoff with jitter, and avoid retrying an operation that is no longer useful. Don't return 429 for unrelated authorization or validation failures; accurate status codes make client behavior and monitoring more reliable.

Rate limiting implementation checklist

Work through this checklist when you design and operate a limit:

  • Identify the constrained resource and the consequence of exhaustion.
  • Select stable characteristics that attackers can't freely choose.
  • Set independent limits for operations with very different costs.
  • Account for retries, idempotency, background jobs, and trusted automation.
  • Start in dry-run mode and measure which users would be limited.
  • Return 429 with actionable retry information.
  • Log the policy, characteristic, remaining capacity, and reset time.
  • Alert on sudden changes in limited traffic and on limiter failures.
  • Test exact boundaries, concurrent requests, clock behavior, and store outages.
  • Review limits as traffic patterns and product plans change.

Frequently asked questions

What is the token bucket rate limiting algorithm?

The token bucket algorithm represents capacity as tokens that accumulate over time up to a configurable maximum. A client that consumes tokens faster than the refill rate is rejected, while idle clients build burst credit. It suits APIs with bursty clients and operations that have different costs.

Which rate limiting algorithm should I use?

Use a fixed window for simple quotas, a sliding window when boundary bursts would be harmful, a token bucket when controlled bursts or variable request costs are useful, and a leaky bucket when outbound traffic must stay smooth. The correct choice depends on the resource and user experience you need to protect.

How does leaky bucket differ from token bucket?

Token bucket accumulates unused capacity as burst credit up to a maximum. Leaky bucket emits work at a constant rate and does not bank burst credit in the queue-based model. Use token bucket for bursty APIs; use leaky bucket to shape traffic to a downstream that cannot absorb spikes.

Why can the fixed window algorithm allow twice the limit?

A fixed window resets its counter at a clock boundary. A client can spend the full quota in the last second of one window and the full quota again in the first second of the next, so twice the configured max can arrive within about one round-trip of the reset.

Should APIs rate limit by IP address?

IP-based limits are a useful anonymous fallback, but they shouldn't be the only identifier. Shared networks can put many users behind one IP, while attackers can rotate addresses. Prefer stable user, account, API key, or tenant identifiers when available.

What HTTP status code should a rate-limited API return?

Return HTTP 429 Too Many Requests. Include enough information for a client to know when to retry, and keep the error body stable and machine-readable.

Application security in your code

Protect your application with Arcjet

Get identity-aware rate limits in your request handlers.