Loading content...
Picture a physical bucket with a small hole at the bottom. Water pours in at irregular rates—sometimes a trickle, sometimes a flood—but the water leaking out through the hole flows at a constant rate determined only by the hole's size and gravity. If water comes in faster than it leaks out, the bucket fills. If the bucket overflows, excess water is lost.
This simple physical model is the Leaky Bucket Algorithm—one of the most elegant and effective traffic shaping mechanisms ever devised. Despite its simplicity, it provides mathematically provable guarantees for traffic conformance and forms the foundation for network QoS implementations worldwide.
By the end of this page, you will deeply understand the Leaky Bucket Algorithm—its conceptual model, mathematical properties, implementation approaches, variations, advantages, limitations, and real-world applications. You will be able to implement leaky bucket in code and reason about its behavior in network traffic scenarios.
The Leaky Bucket Algorithm draws directly from its physical analogy:
Components:
Behavior:
Key Properties of Leaky Bucket:
The leaky bucket has two equivalent interpretations: (1) as a queue that services at a constant rate, and (2) as a counter that decrements at a constant rate. The first models traffic shaping; the second models rate metering/policing. Both are mathematically equivalent but useful in different contexts.
Understanding the leaky bucket mathematically enables precise configuration and analysis.
Core Parameters:
Let us define:
Fundamental Relationships:
| Property | Formula | Interpretation |
|---|---|---|
| Queue Length | q(t) = a(t) - d(t) | Water level = arrivals minus departures |
| Queue Bound | 0 ≤ q(t) ≤ b | Level never negative, never exceeds bucket size |
| Departure Rate | d(t+Δ) - d(t) ≤ r·Δ | Output rate never exceeds leak rate |
| Departure (queue non-empty) | d(t+Δ) - d(t) = r·Δ | When packets are queued, output at full rate |
| Long-term Average | lim(t→∞) a(t)/t ≤ r + ε | Average arrival rate must approach leak rate for stability |
Arrival Curve Constraint:
A traffic flow conforming to a leaky bucket with rate r and burst b satisfies:
a(t) - a(s) ≤ b + r(t - s) for all s ≤ t
This is the arrival curve constraint—it defines the envelope of all conforming traffic. Any traffic within this envelope will never cause overflow.
Delay Bound:
The maximum delay experienced by any packet in a leaky bucket shaper:
D_max = b / r
This is intuitive: the worst case is when the bucket is completely full (b bytes), and all packets must wait for the bucket to drain at rate r.
Example Calculation:
This maximum delay occurs only when the bucket goes from empty to completely full instantly (maximum burst scenario).
The leaky bucket is central to Network Calculus, a mathematical framework for analyzing delay and backlog bounds in networks. The arrival curve and service curve concepts enable compositional analysis of queuing networks, making leaky bucket analysis foundational for network engineering.
The queue-based interpretation of leaky bucket implements traffic shaping. Packets are buffered in a FIFO queue and transmitted at a constant rate.
Algorithm (Pseudo-code):
Leaky Bucket Shaper:
Parameters:
bucket_size: maximum queue length (bytes or packets)
leak_rate: output rate (bytes per second)
State:
queue: FIFO queue of packets
On Packet Arrival(packet):
if queue.size + packet.size > bucket_size:
DROP packet // Overflow
else:
queue.enqueue(packet)
Transmission (runs continuously):
while true:
if queue.not_empty:
packet = queue.dequeue()
transmit(packet)
sleep(packet.size / leak_rate) // Inter-packet gap
else:
sleep(small_interval) // Wait for packets
Implementation Considerations:
1. Timer-Based vs Event-Based:
Timer-based: A periodic timer fires at the leak rate, transmitting one packet (or fixed bytes) each interval.
Event-based: Transmission scheduled based on when the previous packet completes.
2. Byte Mode vs Packet Mode:
Byte mode: Bucket size and leak rate measured in bytes
Packet mode: Bucket size and leak rate measured in packets
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
import timeimport threadingfrom collections import dequefrom typing import Optional class LeakyBucketShaper: """ Implements a Leaky Bucket Traffic Shaper. Packets are queued and released at a constant rate, smoothing bursty traffic into a steady stream. """ def __init__(self, bucket_size_bytes: int, leak_rate_bps: float): """ Initialize the leaky bucket shaper. Args: bucket_size_bytes: Maximum buffer size in bytes leak_rate_bps: Output rate in bytes per second """ self.bucket_size = bucket_size_bytes self.leak_rate = leak_rate_bps self.queue = deque() self.current_size = 0 self.lock = threading.Lock() # Statistics self.total_bytes_in = 0 self.total_bytes_out = 0 self.drops = 0 # Start the leak thread self.running = True self.leak_thread = threading.Thread(target=self._leak_loop) self.leak_thread.daemon = True self.leak_thread.start() def enqueue(self, packet: bytes) -> bool: """ Attempt to add a packet to the bucket. Returns True if successful, False if dropped due to overflow. """ with self.lock: if self.current_size + len(packet) > self.bucket_size: self.drops += 1 return False # Bucket overflow self.queue.append(packet) self.current_size += len(packet) self.total_bytes_in += len(packet) return True def _leak_loop(self): """ Continuously drain the bucket at the leak rate. """ while self.running: with self.lock: if self.queue: packet = self.queue.popleft() self.current_size -= len(packet) self.total_bytes_out += len(packet) # Calculate sleep time based on packet size # This ensures constant byte rate output sleep_time = len(packet) / self.leak_rate else: sleep_time = 0.001 # 1ms poll interval when empty time.sleep(sleep_time) def get_stats(self) -> dict: """Return current statistics.""" with self.lock: return { "queue_depth": self.current_size, "total_in": self.total_bytes_in, "total_out": self.total_bytes_out, "drops": self.drops, "utilization": self.current_size / self.bucket_size, } def stop(self): """Stop the leak thread.""" self.running = False self.leak_thread.join() # Example usageif __name__ == "__main__": # 100 KB bucket, 1 Mbps leak rate shaper = LeakyBucketShaper( bucket_size_bytes=100_000, leak_rate_bps=125_000 # 1 Mbps = 125,000 bytes/sec ) # Simulate bursty traffic for i in range(10): packet = b"x" * 5000 # 5 KB packets if shaper.enqueue(packet): print(f"Packet {i+1}: Enqueued") else: print(f"Packet {i+1}: DROPPED (overflow)") # Let the shaper drain time.sleep(2) print(f"Stats: {shaper.get_stats()}") shaper.stop()Production implementations often use hardware timers, zero-copy buffers, and lock-free data structures for performance. The Linux kernel's traffic control (tc) implements leaky bucket in the 'tbf' (Token Bucket Filter) queuing discipline, which can shape traffic at line rate on multi-gigabit interfaces.
The meter-based interpretation of leaky bucket implements traffic policing. Instead of queueing packets, a counter tracks 'tokens' (credits) that represent permission to send.
Counter-Based Model:
This is mathematically equivalent to the queue model but implementable without actual packet buffering.
| Aspect | Queue Model (Shaper) | Counter Model (Policer) |
|---|---|---|
| What's stored | Actual packets | Byte count (number) |
| On packet arrival | Add packet to queue | Add size to counter |
| 'Leak' operation | Transmit packet | Decrement counter |
| On overflow | Drop/block incoming packet | Mark non-conforming |
| Packet delay | Variable (queue wait) | None |
| Use case | Traffic shaping | Traffic policing/metering |
| Memory requirement | Buffer for all queued packets | Single integer counter |
Metering Algorithm:
Leaky Bucket Meter:
Parameters:
bucket_size: maximum counter value (bytes)
leak_rate: rate of counter decrement (bytes per second)
State:
counter: current fill level (bytes)
last_update: timestamp of last update
On Packet Arrival(packet, current_time):
// First, drain the bucket based on elapsed time
elapsed = current_time - last_update
counter = max(0, counter - leak_rate * elapsed)
last_update = current_time
// Then, check if packet conforms
if counter + packet.size > bucket_size:
return NON_CONFORMING // Drop or mark
else:
counter += packet.size
return CONFORMING
Conforming vs Non-conforming Actions:
| Action | Description | When Used |
|---|---|---|
| Pass | Allow packet unchanged | Conforming traffic |
| Drop | Discard packet | Strict policing |
| Mark | Set DSCP/ECN bits | Re-color for downstream handling |
| Shape | Queue for later transmission | Convert policing to shaping |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
import timefrom enum import Enum class Conformance(Enum): CONFORMING = "green" NON_CONFORMING = "red" class LeakyBucketMeter: """ Implements a Leaky Bucket Traffic Meter/Policer. Uses a counter model to measure traffic conformance without actually buffering packets. """ def __init__(self, bucket_size_bytes: int, leak_rate_bps: float): """ Initialize the leaky bucket meter. Args: bucket_size_bytes: Maximum bucket capacity (burst allowance) leak_rate_bps: Rate at which bucket drains (sustained rate) """ self.bucket_size = bucket_size_bytes self.leak_rate = leak_rate_bps self.counter = 0.0 # Current bucket fill level self.last_update = time.time() # Statistics self.conforming_bytes = 0 self.non_conforming_bytes = 0 def _update_counter(self, current_time: float): """Update counter based on elapsed time (drain the bucket).""" elapsed = current_time - self.last_update self.last_update = current_time # Drain: counter decreases at leak_rate drained = elapsed * self.leak_rate self.counter = max(0.0, self.counter - drained) def meter_packet(self, packet_size: int) -> Conformance: """ Meter a packet and determine its conformance. Args: packet_size: Size of the packet in bytes Returns: Conformance status (CONFORMING or NON_CONFORMING) """ current_time = time.time() self._update_counter(current_time) # Check if packet fits in remaining bucket capacity if self.counter + packet_size <= self.bucket_size: # Conforming: add to bucket self.counter += packet_size self.conforming_bytes += packet_size return Conformance.CONFORMING else: # Non-conforming: packet would overflow bucket self.non_conforming_bytes += packet_size return Conformance.NON_CONFORMING def get_stats(self) -> dict: """Return current statistics.""" return { "bucket_level": self.counter, "bucket_utilization": self.counter / self.bucket_size, "conforming_bytes": self.conforming_bytes, "non_conforming_bytes": self.non_conforming_bytes, "conformance_rate": ( self.conforming_bytes / (self.conforming_bytes + self.non_conforming_bytes + 0.001) ), } # Example: Measuring conformanceif __name__ == "__main__": # 10 KB burst, 10 Kbps sustained meter = LeakyBucketMeter( bucket_size_bytes=10_000, leak_rate_bps=1_250 # 10 Kbps = 1,250 bytes/sec ) # Immediate burst of 5 packets print("=== Burst Test ===") for i in range(5): result = meter.meter_packet(3000) # 3 KB packets print(f"Packet {i+1} (3KB): {result.value}") # Wait for bucket to drain partially print("=== After 5 second wait ===") time.sleep(5) for i in range(3): result = meter.meter_packet(3000) print(f"Packet {i+1} (3KB): {result.value}") print(f"Stats: {meter.get_stats()}")When a policer drops TCP packets, TCP interprets this as congestion and backs off. However, the policer will continue dropping the same percentage of packets even at lower rates, creating a cycle where TCP never stabilizes at the target rate. This 'TCP penalty box' effect is why shaping is often preferred over policing for TCP traffic.
The leaky bucket has several important properties that determine its suitability for different applications.
Output Traffic Characteristics:
Traffic output from a leaky bucket shaper has the following characteristics:
| Property | Value/Behavior | Implication |
|---|---|---|
| Output Rate | Exactly r (when queue non-empty) | Perfectly smooth output |
| Maximum Burst Out | 1 packet (output is never bursty) | No downstream burst stress |
| Maximum Delay | b/r seconds | Bounded, predictable latency |
| Jitter | 0 (when queue non-empty) | Ideal for constant-bitrate applications |
| Buffer Required | b bytes | Fixed, known memory requirement |
| Packet Loss (shaper) | Only when buffer overflows | Minimized with proper sizing |
Burst Handling:
The bucket parameter (b) determines burst tolerance:
Example: Burst Analysis
Configuration: b = 100 KB, r = 1 Mbps (125 KB/s)
| Burst Size | Packets Lost (Policer) | Full Drain Time |
|---|---|---|
| 50 KB | 0 | 0.4 seconds |
| 100 KB | 0 | 0.8 seconds |
| 150 KB | 50 KB | 0.8 seconds |
| 200 KB | 100 KB | 0.8 seconds |
Steady-State Analysis:
In steady state (long-term average):
For TCP traffic, bucket size should be at least Bandwidth × RTT to avoid performance degradation. For a 10 Mbps link with 100ms RTT: b ≥ 10 Mbps × 0.1s = 1 Mb = 125 KB. This allows TCP's congestion window to remain full during normal operation.
The Key Limitation: No Bandwidth Sharing
The fundamental limitation of leaky bucket is that it enforces a strict constant rate regardless of network conditions. Consider:
This 10x slowdown occurs because leaky bucket doesn't allow bursting above the configured rate, even when resources are available. This is why the Token Bucket algorithm (next page) was developed—it allows controlled bursting while still enforcing long-term rate limits.
Leaky bucket is ideal when you need absolutely constant output rate—for example, feeding a hardware interface that cannot accept bursts, or matching the output to a downstream link's exact capacity. For most traffic shaping applications, token bucket provides better performance.
Several variations of the basic leaky bucket address its limitations or adapt it for specific use cases.
1. Dual Leaky Bucket:
Two leaky buckets in series, typically with:
This allows some burstiness while bounding peak rates.
2. Hierarchical Leaky Bucket:
Multiple leaky buckets organized hierarchically:
3. Weighted Leaky Bucket:
Different flows get different 'leak rates' from a shared bucket:
| Variation | Key Difference | Use Case |
|---|---|---|
| Basic Leaky Bucket | Single rate, single bucket | Simple rate limiting |
| Dual Leaky Bucket | Two buckets for peak + average | Allow controlled bursts |
| GCRA (Generic Cell Rate) | Virtual scheduling algorithm | ATM/cell-based networks |
| Hierarchical Leaky Bucket | Parent-child bucket relationships | Multi-tenant rate limiting |
| Leaky Bucket with Priority | Per-priority queues in bucket | QoS with rate limiting |
Generic Cell Rate Algorithm (GCRA):
GCRA is a virtual scheduling variant of leaky bucket used extensively in ATM networks:
GCRA Parameters:
I = Increment (transmission time for one cell)
L = Limit (tolerance for cell delay variation)
State:
TAT = Theoretical Arrival Time (next expected arrival)
On Cell Arrival at time t:
if t >= TAT - L:
TAT = max(TAT, t) + I
return CONFORMING
else:
return NON_CONFORMING
GCRA is mathematically equivalent to leaky bucket but:
GCRA is the algorithm specified in ATM standards (ITU-T I.371, AF-TM-0121) for cell rate conformance. Despite ATM's decline, GCRA concepts live on in modern networking—carriers often use GCRA for Frame Relay and leased line policing.
Leaky bucket algorithms appear throughout networking, though often under different names or combined with other mechanisms.
Application: Network Interface Rate Limiting
Leaky bucket is ideal for rate-limiting traffic sent to a hardware interface:
Linux Traffic Control (tc) Example:
The Linux kernel's TBF (Token Bucket Filter) qdisc can approximate leaky bucket behavior:
# Rate limit eth0 egress to 1 Mbps with 10 KB buffer
# 'peakrate' set equal to 'rate' approximates leaky bucket
tc qdisc add dev eth0 root tbf
rate 1mbit
burst 10kb
latency 100ms
peakrate 1mbit
mtu 1500
# Explanation:
# rate 1mbit - Target rate (leaky rate)
# burst 10kb - Bucket size
# latency 100ms - Max queuing delay (alternative to buffer size)
# peakrate 1mbit - Peak rate equals rate (no bursting)
# mtu 1500 - Maximum packet size
Note: Pure leaky bucket in Linux requires setting peakrate equal to rate. The default TBF without peakrate is actually a token bucket (allows bursting).
In modern systems, pure leaky bucket is rarely used alone. It's typically combined with token bucket (for burst tolerance), priority queuing (for differentiated service), or active queue management (for congestion control). Understanding leaky bucket is essential because it's the foundation these more complex systems build upon.
We've thoroughly explored the Leaky Bucket Algorithm—one of the two fundamental traffic shaping mechanisms that forms the theoretical foundation for network QoS.
What's Next:
The leaky bucket's limitation—inability to utilize available bandwidth above the configured rate—is addressed by the Token Bucket Algorithm. The next page explores how token bucket allows controlled bursting while still enforcing average rate limits, making it the dominant traffic shaping algorithm in modern networks.
You now deeply understand the Leaky Bucket Algorithm—its model, mathematics, implementation, properties, and applications. This foundation prepares you for the Token Bucket Algorithm, which builds on leaky bucket concepts while adding crucial flexibility.