Buffering
Why does a bigger buffer not help?
A buffer decouples the producer's rate from the consumer's rate for a bounded amount of work. Think of the waiting room of a clinic: it lets patients arrive in a burst while the doctor works at a steady speed. The number that matters is not the buffer's size in bytes but its capacity in seconds:
- how much the buffer holds (events)
- how much work comes in per second
- how much work the consumer can finish per second
- work waiting after t seconds of overload
Data table
| Overload λ − μ (events per second) | series | Survival time |
|---|---|---|
| 10 | 600,000 events | 60,000 s |
| 11.2 | 600,000 events | 53,500 s |
| 12.6 | 600,000 events | 47,700 s |
| 14.1 | 600,000 events | 42,500 s |
| 15.8 | 600,000 events | 37,900 s |
| 17.8 | 600,000 events | 33,700 s |
| 20 | 600,000 events | 30,100 s |
| 22.4 | 600,000 events | 26,800 s |
| 25.1 | 600,000 events | 23,900 s |
| 28.2 | 600,000 events | 21,300 s |
| 31.6 | 600,000 events | 19,000 s |
| 35.5 | 600,000 events | 16,900 s |
| 39.8 | 600,000 events | 15,100 s |
| 44.7 | 600,000 events | 13,400 s |
| 50.1 | 600,000 events | 12,000 s |
| 56.2 | 600,000 events | 10,700 s |
| 63.1 | 600,000 events | 9,510 s |
| 70.8 | 600,000 events | 8,480 s |
| 79.4 | 600,000 events | 7,550 s |
| 89.1 | 600,000 events | 6,730 s |
| 100 | 600,000 events | 6,000 s |
| 112 | 600,000 events | 5,350 s |
| 126 | 600,000 events | 4,770 s |
| 141 | 600,000 events | 4,250 s |
| 158 | 600,000 events | 3,790 s |
| 178 | 600,000 events | 3,370 s |
| 200 | 600,000 events | 3,010 s |
| 224 | 600,000 events | 2,680 s |
| 251 | 600,000 events | 2,390 s |
| 282 | 600,000 events | 2,130 s |
| 316 | 600,000 events | 1,900 s |
| 355 | 600,000 events | 1,690 s |
| 398 | 600,000 events | 1,510 s |
| 447 | 600,000 events | 1,340 s |
| 501 | 600,000 events | 1,200 s |
| 562 | 600,000 events | 1,070 s |
| 631 | 600,000 events | 951 s |
| 708 | 600,000 events | 848 s |
| 794 | 600,000 events | 755 s |
| 891 | 600,000 events | 673 s |
| 1,000 | 600,000 events | 600 s |
| 1,120 | 600,000 events | 535 s |
| 1,260 | 600,000 events | 477 s |
| 1,410 | 600,000 events | 425 s |
| 1,580 | 600,000 events | 379 s |
| 1,780 | 600,000 events | 337 s |
| 2,000 | 600,000 events | 301 s |
| 2,240 | 600,000 events | 268 s |
| 2,510 | 600,000 events | 239 s |
| 2,820 | 600,000 events | 213 s |
| 3,160 | 600,000 events | 190 s |
| 3,550 | 600,000 events | 169 s |
| 3,980 | 600,000 events | 151 s |
| 4,470 | 600,000 events | 134 s |
| 5,010 | 600,000 events | 120 s |
| 5,620 | 600,000 events | 107 s |
| 6,310 | 600,000 events | 95.1 s |
| 7,080 | 600,000 events | 84.8 s |
| 7,940 | 600,000 events | 75.5 s |
| 8,910 | 600,000 events | 67.3 s |
| 10K | 600,000 events | 60 s |
| 11.2K | 600,000 events | 53.5 s |
| 12.6K | 600,000 events | 47.7 s |
| 14.1K | 600,000 events | 42.5 s |
| 15.8K | 600,000 events | 37.9 s |
| 17.8K | 600,000 events | 33.7 s |
| 20K | 600,000 events | 30.1 s |
| 22.4K | 600,000 events | 26.8 s |
| 25.1K | 600,000 events | 23.9 s |
| 28.2K | 600,000 events | 21.3 s |
| 31.6K | 600,000 events | 19 s |
| 35.5K | 600,000 events | 16.9 s |
| 39.8K | 600,000 events | 15.1 s |
| 44.7K | 600,000 events | 13.4 s |
| 50.1K | 600,000 events | 12 s |
| 56.2K | 600,000 events | 10.7 s |
| 63.1K | 600,000 events | 9.51 s |
| 70.8K | 600,000 events | 8.48 s |
| 79.4K | 600,000 events | 7.55 s |
| 89.1K | 600,000 events | 6.73 s |
| 100K | 600,000 events | 6 s |
| 10 | 6,000,000 events | 600,000 s |
| 11.2 | 6,000,000 events | 535,000 s |
| 12.6 | 6,000,000 events | 477,000 s |
| 14.1 | 6,000,000 events | 425,000 s |
| 15.8 | 6,000,000 events | 379,000 s |
| 17.8 | 6,000,000 events | 337,000 s |
| 20 | 6,000,000 events | 301,000 s |
| 22.4 | 6,000,000 events | 268,000 s |
| 25.1 | 6,000,000 events | 239,000 s |
| 28.2 | 6,000,000 events | 213,000 s |
| 31.6 | 6,000,000 events | 190,000 s |
| 35.5 | 6,000,000 events | 169,000 s |
| 39.8 | 6,000,000 events | 151,000 s |
| 44.7 | 6,000,000 events | 134,000 s |
| 50.1 | 6,000,000 events | 120,000 s |
| 56.2 | 6,000,000 events | 107,000 s |
| 63.1 | 6,000,000 events | 95,100 s |
| 70.8 | 6,000,000 events | 84,800 s |
| 79.4 | 6,000,000 events | 75,500 s |
| 89.1 | 6,000,000 events | 67,300 s |
| 100 | 6,000,000 events | 60,000 s |
| 112 | 6,000,000 events | 53,500 s |
| 126 | 6,000,000 events | 47,700 s |
| 141 | 6,000,000 events | 42,500 s |
| 158 | 6,000,000 events | 37,900 s |
| 178 | 6,000,000 events | 33,700 s |
| 200 | 6,000,000 events | 30,100 s |
| 224 | 6,000,000 events | 26,800 s |
| 251 | 6,000,000 events | 23,900 s |
| 282 | 6,000,000 events | 21,300 s |
| 316 | 6,000,000 events | 19,000 s |
| 355 | 6,000,000 events | 16,900 s |
| 398 | 6,000,000 events | 15,100 s |
| 447 | 6,000,000 events | 13,400 s |
| 501 | 6,000,000 events | 12,000 s |
| 562 | 6,000,000 events | 10,700 s |
| 631 | 6,000,000 events | 9,510 s |
| 708 | 6,000,000 events | 8,480 s |
| 794 | 6,000,000 events | 7,550 s |
| 891 | 6,000,000 events | 6,730 s |
| 1,000 | 6,000,000 events | 6,000 s |
| 1,120 | 6,000,000 events | 5,350 s |
| 1,260 | 6,000,000 events | 4,770 s |
| 1,410 | 6,000,000 events | 4,250 s |
| 1,580 | 6,000,000 events | 3,790 s |
| 1,780 | 6,000,000 events | 3,370 s |
| 2,000 | 6,000,000 events | 3,010 s |
| 2,240 | 6,000,000 events | 2,680 s |
| 2,510 | 6,000,000 events | 2,390 s |
| 2,820 | 6,000,000 events | 2,130 s |
| 3,160 | 6,000,000 events | 1,900 s |
| 3,550 | 6,000,000 events | 1,690 s |
| 3,980 | 6,000,000 events | 1,510 s |
| 4,470 | 6,000,000 events | 1,340 s |
| 5,010 | 6,000,000 events | 1,200 s |
| 5,620 | 6,000,000 events | 1,070 s |
| 6,310 | 6,000,000 events | 951 s |
| 7,080 | 6,000,000 events | 848 s |
| 7,940 | 6,000,000 events | 755 s |
| 8,910 | 6,000,000 events | 673 s |
| 10K | 6,000,000 events | 600 s |
| 11.2K | 6,000,000 events | 535 s |
| 12.6K | 6,000,000 events | 477 s |
| 14.1K | 6,000,000 events | 425 s |
| 15.8K | 6,000,000 events | 379 s |
| 17.8K | 6,000,000 events | 337 s |
| 20K | 6,000,000 events | 301 s |
| 22.4K | 6,000,000 events | 268 s |
| 25.1K | 6,000,000 events | 239 s |
| 28.2K | 6,000,000 events | 213 s |
| 31.6K | 6,000,000 events | 190 s |
| 35.5K | 6,000,000 events | 169 s |
| 39.8K | 6,000,000 events | 151 s |
| 44.7K | 6,000,000 events | 134 s |
| 50.1K | 6,000,000 events | 120 s |
| 56.2K | 6,000,000 events | 107 s |
| 63.1K | 6,000,000 events | 95.1 s |
| 70.8K | 6,000,000 events | 84.8 s |
| 79.4K | 6,000,000 events | 75.5 s |
| 89.1K | 6,000,000 events | 67.3 s |
| 100K | 6,000,000 events | 60 s |
The four decisions about any buffer
1. Where does it live?
| Location | Capacity | Survives a crash? | Cost |
|---|---|---|---|
| In-process memory | MB to GB | No | fastest; puts pressure on the garbage collector; lost on restart |
| Local disk (spill) | GB to TB | Yes (same machine only) | disk I/O; tied to one machine |
| Broker | TB and more | Yes | infrastructure to run; a network hop |
| Object store | effectively unlimited | Yes | latency; cost per request |
Moving down the table, durability goes up and speed goes down. That is no surprise. But note one thing: an in-memory buffer is a hidden durability decision. If a record is acknowledged into an in-memory buffer and the process restarts, the record is lost. This is silent data loss, and no metric shows it. If your producer is told "OK" before the buffer is durable, you have chosen at-most-once delivery (a message may be lost but never repeated) without saying so. This is the second half of Ravi's problem: the lost in-flight work.
2. Bounded or unbounded?
Always bounded. An unbounded queue is a latency bomb. It accepts work that it will never process in time, so records reach the consumer already stale and useless. The out-of-memory kill comes anyway, only later and with more work destroyed. The only thing an unbounded buffer buys is a longer wait before the failure, and the failure is total instead of partial.
3. What is the full-policy?
There are four choices:
- Block the producer (this becomes backpressure, below).
- Drop-newest (reject the new arrival; this is load shedding, below).
- Drop-oldest (prefer fresh data over complete data).
- Spill to a bigger, slower place.
Drop-oldest is underused. Use it when a newer record makes an older one worthless: metrics, sensor readings, cache invalidations, "who is online" status, price ticks. Dropping the head of the queue (the oldest) is then strictly better than dropping the tail (the newest). For a keyed buffer where only the latest value per key matters, dropping the oldest per key is better still: you reduce volume without losing any entity (a product, a user, a sensor).
4. Head-of-line blocking?
In a single FIFO (first in, first out) buffer, one slow item stops everything behind it. Giving each key or each tenant (customer) its own sub-buffer fixes this, at the cost of managing N buffers. This is the idea of bulkheading.
Bufferbloat: the thing most people get wrong
Bigger buffers feel safer. Past a certain size, they are not.
A large buffer that stays full gives you the latency of a full buffer all the time, and no extra throughput. Throughput was always μ. The buffer never changed it.
The correct way to think about sizing: a buffer should be sized for absorbing bursts, not for sustained overload. Use bandwidth-delay-product logic (the amount of data "in the pipe" during one round trip): the buffer must cover the length of the burst plus the reaction time of whatever downstream mechanism will actually fix the imbalance. If your buffer is chronically more than about half full, it is not protecting you. It is a queue on which you pay extra latency all the time. The right move is a smaller buffer plus an earlier signal, not a bigger buffer.
Two mitigations are worth knowing by name:
- CoDel (controlled delay). Measure the minimum queueing delay over a sliding time interval. If even the minimum stays above a target, the queue never drains: it is a standing queue, not a burst. Then start dropping. This is elegant because it separates a good burst-absorbing buffer from a bad standing one, which raw queue depth cannot do.
- Deadline-aware buffering. Stamp each record with an expiry time when it enters the buffer. When you take it out, drop it if it is already useless. This saves the consumer's scarce capacity for records somebody will still use. It is cheap to build and surprisingly effective.