Leaky Bucket

The leaky bucket is a rate-limiting and traffic-shaping algorithm that converts an arbitrary input stream — bursty, idle, or anything in between — into a perfectly smooth output at a constant rate. Conceptually it is a fixed-capacity bucket that drains continuously at a configured rate r; arriving requests (or packets, or jobs) fill the bucket; when the bucket overflows on insertion, the request is rejected (or shaped, depending on variant). Its defining property is no bursts at the output: the downstream sees exactly r events per second regardless of input shape, in contrast to Token Bucket, which permits bursts up to the bucket capacity. The meter formulation was introduced by Jonathan S. Turner in 1986 — he described “a counter associated with each user … incremented whenever the user sends a packet and … decremented periodically” (Wikipedia, Leaky bucket; Turner 1986) — and it is now the conceptual foundation of the Generic Cell Rate Algorithm (GCRA) standardized for ATM. The queue formulation (the literal bucket-with-a-hole) is the basis of Linux’s bucket-style tc shapers and the smoothing primitive behind production rate limiters that must protect a fragile downstream from burst load. These two formulations behave differently and §6 dissects the difference in detail.

1. Intuition — A Bucket With a Hole

Imagine a literal bucket with a small hole in the bottom. Water leaks out at a fixed rate r (say 1 cup per second). The bucket has finite capacity B (say 10 cups).

  • Anyone can pour water in at any rate they want.
  • The water leaves the bucket at exactly r, regardless of how fast it’s poured in.
  • If you pour faster than r, the bucket fills up.
  • Once the bucket is full and you try to pour more in, the new water spills onto the floor — that’s a rejected request.

Two parameters: drain rate r and capacity B.

What the downstream observer sees coming out the hole is a steady drip-drip-drip at rate r — never any bursts, no matter how chaotic the input was. That’s the algorithm’s purpose. It’s a smoother.

The two flavors of “leaky bucket”

This is where confusion enters interviews. The literature uses “leaky bucket” for two related but distinct algorithms:

  1. Leaky bucket as a queue (the literal bucket model above). Requests enter a FIFO queue of bounded size B; the queue is dequeued at rate r. Output is steady; if the queue is full, new arrivals are dropped. This is the traffic-shaping flavor — output rate is enforced by delaying requests in the queue.

  2. Leaky bucket as a meter (a counter, not a queue). A counter that increments by 1 on each arrival and decrements at rate r continuously. If the counter would exceed capacity B, the request is rejected. Output is not smoothed — accepted requests proceed immediately. This is mathematically equivalent to a token bucket (but inverted: counter goes up instead of down).

Tanenbaum’s Computer Networks describes the queue version precisely: “The leaky bucket consists of a finite queue. When a packet arrives, if there is room in the queue, it is appended to the queue; otherwise, it is discarded. At every clock tick, one packet is transmitted (unless the queue is empty)” — a pure traffic shaper with a rigid constant output rate. The meter version is the one the ATM standards formalize as GCRA. Some other texts conflate them. For interview clarity, sketch the algorithm — don’t rely on the name. When engineers say “leaky bucket” today they usually mean the queue / smoothing variant; when networking standards say “leaky bucket” they usually mean the meter / GCRA variant.

A subtle but useful unifying fact: the queue variant is a special case of the meter variant — specifically, the meter with limit L = 0 (zero burst tolerance) and conformance tested at the finest time granularity reduces to “admit at most one unit per 1/r interval,” which is exactly what a constant-drain FIFO enforces (Wikipedia, Leaky bucket). The difference the reader must hold onto is what happens to a non-conforming request: the queue delays it (it waits its turn and is emitted later, smoothed), whereas the meter rejects it outright while letting conforming requests pass immediately and unsmoothed.

The discussion below focuses on the queue-based smoothing variant, which is the canonical “leaky bucket” most interview questions probe and which materially differs from Token Bucket in observable behavior. The meter variant is covered in §6 as it’s algorithmically dual.

2. Tiny Worked Example (B = 5 capacity, r = 1 request/second)

Bucket starts empty. Each row records an arrival or a “tick” of the leak.

Time (s)EventBucket fill beforeActionBucket afterOutput?
0.0arrival0enqueue1
0.1arrival1enqueue2
0.2arrival2enqueue3
0.3arrival3enqueue4
0.4arrival4enqueue5
0.5arrival5REJECT (overflow)5
1.0leak tick5dequeue 14request 0 emitted
2.0leak tick4dequeue 13request 1 emitted
2.5arrival3enqueue4
3.0leak tick4dequeue 13request 2 emitted
4.0leak tick3dequeue 12request 3 emitted
5.0leak tick2dequeue 11request 4 emitted
6.0leak tick1dequeue 10request 2.5-arrival emitted
7.0leak tick0(empty, no-op)0

Two things to notice:

  1. The output is perfectly steady. Once requests start leaking, exactly one comes out every 1.0 seconds — regardless of the bursty input.
  2. The 6th arrival at time 0.5s was rejected. The bucket overflowed because requests came in faster than they could leak out, and the queue was already at capacity 5.

In contrast, a Token Bucket with the same parameters would have allowed all 5 arrivals at time 0.0–0.4s through immediately, then rejected the 6th — but those 5 would have been emitted to the downstream as a burst, hitting the downstream within 0.5 seconds. The leaky bucket makes the downstream wait a full 5 seconds for those 5 requests to arrive, smoothing the burst into a steady drip.

That’s the core behavioral difference. The two algorithms admit roughly the same long-run rate (r requests/s, with burst size B); they differ in when accepted requests reach the downstream.

3. The Algorithm — Bounded FIFO Queue with Constant-Rate Drain

class LeakyBucket(rate r, capacity B):
    queue          = FIFO queue
    last_leak_time = now()

    function arrive(request):
        leak()                                  # update queue based on time elapsed
        if len(queue) < B:
            queue.append(request)               # admit
            return ACCEPTED
        return REJECTED                          # bucket full → drop

    function leak():
        now = current_time()
        elapsed = now - last_leak_time
        leaks = floor(elapsed * r)
        for _ in range(leaks):
            if queue:
                req = queue.pop_front()
                emit_to_downstream(req)
        last_leak_time += leaks / r              # advance virtual clock

3.1 Lazy vs eager leak

The pseudocode above is lazyleak() is called on each arrival, and emission to the downstream happens during the call. This works if the downstream tolerates being called from inside an “arrive” handler.

In practice (especially when each emission is a network send that must happen on a timer), you need an eager version: a separate thread or scheduled task that fires every 1/r seconds and emits one queued request at a time:

function tick():        # called every 1/r seconds by a scheduler
    if queue:
        req = queue.pop_front()
        emit_to_downstream(req)

Both architectures are correct; the lazy version is simpler if the request handling is synchronous, the eager version is necessary if you want the smoothed timing of emission rather than just the smoothed rate.

3.2 Variable-cost requests

If requests have different “sizes” (e.g., a write costs 5 units, a read costs 1 unit), the bucket measures work units not requests, and the queue might hold partial requests. In packet-shaping flavors the bucket measures bytes; in API rate limiters it measures cost.

function arrive(request, cost):
    leak()
    if water + cost <= B:
        water += cost
        queue.append(request)
        return ACCEPTED
    return REJECTED

(In this formulation water tracks the total cost of queued requests rather than len(queue).)

4. Python Implementation

import time
from collections import deque
from threading import Lock
 
 
class LeakyBucket:
    """Queue-based leaky bucket — smooths bursty input into rate-r output.
 
    Parameters
    ----------
    rate : float
        Emission rate in requests per second.
    capacity : int
        Maximum queued requests; arrivals while at capacity are rejected.
    """
 
    def __init__(self, rate: float, capacity: int):
        if rate <= 0 or capacity <= 0:
            raise ValueError("rate and capacity must be positive")
        self.rate = rate
        self.capacity = capacity
        self.queue: deque = deque()
        self.last_leak = time.monotonic()
        self.lock = Lock()
 
    def _leak(self) -> None:
        """Internal: drain the queue based on elapsed time since last leak."""
        now = time.monotonic()
        elapsed = now - self.last_leak
        leaks = int(elapsed * self.rate)
        if leaks > 0:
            for _ in range(min(leaks, len(self.queue))):
                self.queue.popleft()      # in a real impl, emit downstream
            # Advance the virtual leak clock by exactly the integer number of
            # leak ticks consumed (so fractional time accrues for next call).
            self.last_leak += leaks / self.rate
 
    def arrive(self, request) -> bool:
        """Try to admit a request. Returns True on accept, False on overflow."""
        with self.lock:
            self._leak()
            if len(self.queue) < self.capacity:
                self.queue.append(request)
                return True
            return False
 
    def __len__(self) -> int:
        with self.lock:
            self._leak()
            return len(self.queue)
 
 
# --- demo -----------------------------------------------------------------
if __name__ == "__main__":
    bucket = LeakyBucket(rate=2.0, capacity=5)     # 2 emissions/sec, queue 5
    for i in range(10):
        ok = bucket.arrive(f"req-{i}")
        print(f"t={time.monotonic():.2f}: req-{i}{'OK' if ok else 'REJECTED'}")
        time.sleep(0.1)
    time.sleep(3.0)
    print(f"after 3s, bucket holds {len(bucket)} requests")

time.monotonic() is mandatory — see the corresponding pitfall in Token Bucket §9.1: wall-clock jumps from NTP would either grant a free emission or stall the leak.

In a real system, the popleft would push the request onto a downstream channel (HTTP client pool, database connection, downstream API). If the downstream itself is the bottleneck, additional concurrency control around the emit step is needed.

4.1 Distributed leaky bucket

For multi-server enforcement of a single global rate, the bucket state lives in Redis (or similar) and the lazy-leak math is performed in a Lua script for atomicity. The sketch below uses a single integer count and a last_leak timestamp:

-- KEYS[1] = bucket key  ARGV: now, rate, cap
local data = redis.call('HMGET', KEYS[1], 'count', 'last')
local count = tonumber(data[1]) or 0
local last  = tonumber(data[2]) or tonumber(ARGV[1])
local elapsed = tonumber(ARGV[1]) - last
local leaks = math.floor(elapsed * tonumber(ARGV[2]))
count = math.max(0, count - leaks)
local last_advance = last + leaks / tonumber(ARGV[2])
local cap = tonumber(ARGV[3])
local accepted = 0
if count < cap then
    count = count + 1
    accepted = 1
end
redis.call('HMSET', KEYS[1], 'count', count, 'last', last_advance)
redis.call('PEXPIRE', KEYS[1], 60000)
return accepted

Note this distributed version is a leaky bucket as a meter (counter only — accepted requests proceed immediately), not a queue with delayed emission. True smoothed-emission across distributed servers is much harder because emission timing must be coordinated.

5. Complexity

OperationTimeSpace
arrive (lazy leak)O(k) where k is the number of leaks since last callO(B)
arrive (eager leak with timer)O(1)O(B)
tick (eager)O(1)
Total memoryO(B) per bucket (the queued requests themselves)

The “O(k) on arrive” caveat for the lazy version is annoying in pathological cases — if no arrivals occur for a long time, the next arrive may need to drain thousands of leaks at once. In production you either: (a) cap the inner drain loop (min(leaks, capacity) in the implementation above — at most B drains per call), or (b) use eager mode with an external scheduler.

6. Leaky Bucket vs Token Bucket — Mathematical Duality and Behavioral Difference

The two algorithms are often described as “duals”:

AspectLeaky bucket (queue)Token bucket
What flows inRequestsTokens
What flows outRequests (at rate r)Requests consume tokens; tokens never explicitly “flow out”
What’s boundedQueue length ≤ BToken count ≤ B
Empty bucket meansIdle queue → no emissionNo tokens → reject (or wait)
Full bucket meansReject new arrivalsRefills are no-op
Output rateConstant rVariable (up to r long-run, can burst to B)
Output bursts?NeverYes (up to B back-to-back)
Used forTraffic shaping, smoothingRate limiting allowing bursts

6.1 Why this matters in system design

The practical difference plays out when the downstream system has burst sensitivity:

  • A legacy database that can handle 100 QPS but crashes at 200 QPS → use leaky bucket. The token bucket would let a user accumulate 100 tokens during idle time and then dump 200 requests in a half-second window, swamping the DB. The leaky bucket guarantees “at most 100 QPS leaves my limiter, ever.”

  • A user-facing API where bursty client behavior is the norm and the backend is elastic → use token bucket. Forcing every burst to be queued for several seconds makes the API feel laggy. The token bucket admits bursts, accepts the downstream load, and trusts the backend’s autoscaling to absorb it.

  • A network egress link with a fixed bandwidth budget → use leaky bucket (this is what tc qdisc rate does). You need the output to never exceed the link rate, period.

6.2 GCRA — leaky bucket as a meter, equivalent to token bucket

The Generic Cell Rate Algorithm (GCRA), standardized for ATM by the ITU-T (I.371) and the ATM Forum’s Traffic Management specification, is the meter flavor of leaky bucket: a counter that increments by 1 per arrival and conceptually drains at rate r. Unlike the queue version, accepted arrivals proceed immediately — the counter merely tracks “how full has the bucket been recently?” The ATM standards actually define GCRA via two provably-equivalent formulations: the continuous-state leaky bucket (simulate a bucket of capacity T + τ that drains one unit per time unit and increments by T per conforming cell) and the virtual scheduling algorithm (track a theoretical arrival time and avoid simulating the drip), the latter being cheaper because it does no work during idle periods (Wikipedia, Generic cell rate algorithm).

In the virtual-scheduling form, each request arrives at time τ and the bucket stores a single state variable, the theoretical arrival time (TAT) — the earliest time the next cell may conform. On a request arriving at actual time τ:

if τ < TAT - L:                      # arrival too early → bucket would overflow → reject
    return REJECTED
TAT = max(τ, TAT) + I                # advance the next-conforming time by one increment
return ACCEPTED

where I is the inter-arrival increment (1/r, called T in the ATM spec) and L is the limit/tolerance corresponding to burst capacity B (called τ — tau — in the ATM spec; the symbol clash with the arrival timestamp is unfortunate, which is exactly why the spec keeps the two names distinct). The reject test τ < TAT − L is the same inequality the standard writes as “the cell is non-conforming iff actual arrival time < TAT − τ” (Wikipedia, GCRA).

GCRA is mathematically equivalent to a token bucket with the same parameters — same admission decisions, same long-run rate, same burst limit (Wikipedia, Leaky bucket). Its advantage is state size: one timestamp instead of a (token-count, timestamp) pair, and no background drip process. This is why it is the algorithm of choice for Redis-backed limiters such as Brandur Leach’s redis-cell module, which exposes GCRA as a single atomic Redis command (Leach, Rate Limiting, Cells, and GCRA; brandur/redis-cell).

Uncertain uncertain

Verify: the often-repeated claim that Stripe’s production API rate limiter is GCRA-based. Reason: Stripe’s own engineering blog states plainly “we use the token bucket algorithm to do rate limiting” and never mentions GCRA (Stripe, Scaling your API with rate limiters). The GCRA-on-Redis writeup widely cited in this context is Brandur Leach’s personal post — Leach worked at Stripe, which is likely the source of the conflation, but the public record attributes token bucket to Stripe and GCRA to Leach’s redis-cell. To resolve: treat “Stripe uses GCRA” as unsupported and cite token bucket for Stripe; cite Leach/redis-cell for the GCRA-on-Redis pattern. (Earlier revisions of this note asserted “Stripe documented their API limiter as GCRA-based” — that was wrong and has been corrected.)

So: the leaky bucket as a meter is the dual of the token bucket. The leaky bucket as a queue is something distinct — neither admits the same patterns the token bucket does. In interviews, draw the algorithm.

7. Production Use Cases

  • Linux tc qdisc rate (ratectl) and HTB / HFSC. Kernel-level packet shaping that uses leaky-bucket-style queues to enforce bandwidth limits. Outgoing packets queue and are released at the configured rate. The shape of the released traffic is smoothed.

  • ATM networks. The original deployment from Turner 1986 — leaky bucket / GCRA enforced cell rate limits per virtual circuit. Largely historical (ATM is dead in commercial WAN), but the algorithm survived as networking textbook canon.

  • Network egress smoothing in cloud VMs. AWS EC2 and other clouds implement bandwidth-quota egress with leaky-bucket-style shaping at the hypervisor level so a noisy neighbor’s burst doesn’t saturate the host NIC.

  • API gateways for fragile backends. When fronting a legacy SOAP service or a single-threaded microservice that genuinely can’t tolerate bursts, leaky-bucket queueing protects the downstream. (Less common today than token bucket, because most modern backends are autoscaling.)

  • Mail-sending rate limiters. Outbound SMTP gateways use leaky-bucket-shaped sending to comply with recipient-server rate limits and reduce spam blacklist risk. (One sender may have a queue full of mail; releasing it at 10 messages/minute prevents the recipient from rate-limiting back.)

  • Disk write quota in some filesystems. Smoothing write-IOPS so a burst doesn’t saturate the disk’s write-cache and affect read latency.

  • Job dispatchers with strict pacing requirements. Cron-like systems that need to dispatch ~N jobs/minute spread evenly rather than all at once.

Uncertain uncertain

Verify: the internal shaping algorithm and current default parameters of (a) AWS EC2 hypervisor-level egress bandwidth shaping and (b) Linux tc qdiscs. Reason: cloud-provider traffic-shaping internals are not publicly specified at the algorithm level, and tc defaults drift across kernel versions — these are point-in-time, vendor-internal facts, not retrievable from a single authoritative primary spec. To resolve: read the current tc-tbf(8)/tc-htb(8) man pages for tc specifics and AWS’s current networking documentation at quote time. What is well-established and not in doubt: Linux tc token-bucket-filter (tbf) and HTB qdiscs implement bucket-style shaping, and ATM used GCRA per-virtual-circuit — those framings are safe; only the specific numbers are volatile.

8. Variants

8.1 Hierarchical Leaky Bucket / HTB (Hierarchical Token Bucket — somewhat misnamed)

Linux’s tc htb qdisc maintains a tree of buckets where children share their parent’s rate. Despite the name, it is a hybrid that can implement both leaky-bucket-style smoothing and token-bucket-style bursting depending on configuration.

8.2 Weighted Fair Queueing (WFQ)

Generalizes the single-queue leaky bucket to multiple input flows, each with its own queue, dequeued round-robin (weighted by per-flow priority). Provides per-flow fairness in addition to rate enforcement.

8.3 GCRA — the meter formulation (§6.2)

Mathematically a token bucket; called “leaky bucket” in ATM standards.

8.4 Fluid-Model Leaky Bucket

Continuous-time formulation used in queueing theory: water level evolves as dW/dt = arrivals(t) − r, capped at B. Useful for analytical bounds; not directly implementable.

8.5 Discrete-Time Leaky Bucket

The variant in §3 — the implementable version. Time is sampled at request arrival, and leak is computed lazily.

8.6 Soft / Adaptive Leaky Bucket

Variants that adjust r dynamically based on downstream feedback (e.g., AIMD — additive increase, multiplicative decrease). Used in some congestion-control schemes; conceptually leaky-bucket but with a feedback loop.

8.7 Per-Flow vs Global Bucket Scope

A leaky bucket per user gives per-user smoothing; a global bucket gives system-wide rate cap. Both are used; the choice depends on whether the protected resource is per-user or shared. Misconfiguring scope is a classic interview trap.

9. Pitfalls

9.1 Latency Inflation from Queue Depth

A leaky bucket of capacity B at rate r imposes a worst-case delay of B/r on every request that has to wait through the queue. With B=100 and r=10/s the worst-case wait is 10 seconds. Interactive clients may time out. Always size B to the maximum acceptable latency × rate, not to the burst size you’d like to absorb.

9.2 Confusing Queue and Meter Variants

The single most common interview confusion. State which variant you mean. The queue version delays requests; the meter version rejects them while letting accepted requests proceed immediately. They have different observable behaviors despite both being called “leaky bucket.”

9.3 Time Source Choice

Same pitfall as token bucket §9.1: using wall-clock time means an NTP backward jump can either freeze the leak or allow free draining. Use a monotonic clock.

9.4 Concurrency

The bucket is shared mutable state across worker threads. Without synchronization, the queue can be enqueued past capacity or two threads can dequeue the same request. Lock the bucket, or use Redis’s atomic Lua scripts.

9.5 The “Big Idle” Problem (Lazy Leak)

If the bucket has been idle for an hour, the next arrival’s lazy leak() call wants to drain potentially r·3600 leaks. Bound this either by capping the inner loop at B (anything beyond that would have left the queue empty anyway) or by switching to eager leak with a scheduler thread.

9.6 Overflow vs Backpressure Semantics

“Reject on overflow” assumes the upstream caller can handle a 429-equivalent. If they can’t, you need backpressure — block the caller until queue space frees up. This adds blocking semantics (and potential deadlock); pick whichever model the upstream expects.

9.7 Per-User Bucket State Explosion

A leaky bucket per user with 10⁸ users is 10⁸ buckets. Each bucket carries a queue of up to B requests + bookkeeping. Even at 50 bytes/bucket, that’s 5 GB of state — not free. Use TTL-based eviction of idle buckets, or shard the bucket array.

9.8 Forgetting the Difference From Token Bucket

The classic interview misstep: candidate proposes “leaky bucket” but describes token-bucket behavior (allowing bursts). Diagnose by asking “if a user is idle for an hour, can they then send 100 requests in 1 second?” — if yes, that’s token bucket; if no, that’s leaky bucket.

9.9 Output-Side Coordination Across Distributed Nodes

True smoothed emission across N nodes requires global coordination of when requests fire — not just whether. Most “distributed leaky buckets” only enforce admission rate (the meter variant); the smoothing property is an artifact of single-node enforcement. If you need globally-smoothed emission (e.g., outbound mail to a single recipient from many SMTP nodes), use a single-node coordinator or a leader-elected dispatcher.

10. Diagram — Bucket-with-Hole

flowchart TD
    IN["Bursty arrivals<br/>(unconstrained input rate)"] --> Q[("Queue<br/>capacity B<br/>current = w")]
    Q -->|"if w == B → reject"| REJECT["Rejected (HTTP 429)<br/>or overflow drop"]
    Q -.->|leak at rate r| HOLE["Constant-rate output<br/>(at most r per second)"]
    HOLE --> DOWN["Smoothed downstream<br/>(no bursts)"]

What this diagram shows. The vertical “bucket” holds queued requests up to capacity B. New arrivals push onto the top; if the bucket is full, they spill (rejected). Independent of arrival rate, requests leak out the bottom at exactly r per second, producing a downstream stream with no bursts at all. The arrow from bucket to “rejected” only fires when the bucket is at capacity at arrival time — so the algorithm’s two configurable parameters, r and B, jointly govern (a) the long-run rate the downstream sees and (b) the worst-case input-burst size that can be absorbed before requests start being dropped. Compare to the Token Bucket diagram: the bucket is filled by the refill source there and drained by requests, the inverse direction. That topological inversion is the duality.

11. Common Interview Problems

ProblemWhat’s tested
Design a Rate Limiter (general)Compare leaky vs token vs sliding-window; explain when each fits
Design a Traffic Shaper for a fragile downstreamThe smoothing property is the right answer here
Design a Notification Service with per-user smoothingPer-user leaky bucket; latency tradeoff vs reject
Design Network Egress Bandwidth LimiterLeaky bucket on bytes/sec at the edge
Design an Outbound Email SystemLeaky bucket per recipient-domain to comply with their rate limits
Design a Job Scheduler with PacingLeaky bucket as the dispatch primitive
Compare: Token Bucket vs Leaky BucketClassic open-ended sysdesign question — see §6

For coding rounds, the leaky bucket implementation in §4 is the expected answer. Bonus points for distinguishing the queue and meter variants without being prompted.

12. Open Questions

  • Are there workloads where leaky-bucket smoothing measurably helps a modern autoscaling backend (vs token-bucket) — i.e., where smoothing prevents a scale-up event the system would otherwise have to do? Evidence is anecdotal.

  • Has anyone built a self-tuning leaky bucket where r adapts to downstream backpressure signals (à la TCP’s congestion window), and does that beat fixed-rate provisioning in production? AIMD-style controllers exist in academia but are rare in commercial limiters.

  • How does the GCRA reformulation’s smaller state footprint actually translate at modern memory prices — is anyone choosing GCRA over token bucket for the memory savings, or only for the algorithmic elegance?

13. See Also