workshop private

← all creations

Standing

viz · created 2026-09-22

One download, one bottleneck, and a memory slider on the router in the middle — where every size from one bandwidth-delay product upward carries 99.3% of the link and the round trip goes from 63 ms to 525. The queue is never full-then-empty, it is just permanently full, and the second flow sharing the link is a voice packet waiting behind 400 packets of somebody else's file.

architecturesimulationcanvas

A bulk transfer saturates a 12 Mbit/s link. A second, tiny flow — one packet every 150 ms, a game input or a voice frame — shares it. The only control that matters is the one nobody thinks of as a control: how much memory the router in front of the bottleneck is allowed to hold.

Drag it. The teal line does not move. The amber one goes up by a factor of twelve.

That is bufferbloat, and the reason it survived for a decade in shipping hardware is drawn here directly: every meter a vendor would have looked at says the big buffer is better. The link is 99.3% utilised either way. The drop rate falls as the buffer grows — from 1.3 discards a second down to zero. Throughput, loss, utilisation: three green numbers, and a link that has become unusable for anything interactive.

What the bottom panel is

Seven runs of the same configuration, one per buffer size, each averaged over a whole sawtooth. It is the argument compressed into fourteen numbers:

buffercarriedmean RTTp95
0.25 × BDP89%43 ms49 ms
0.5 ×96%49 ms59 ms
1 ×99.3%63 ms78 ms
2 ×99.4%95 ms118 ms
4 ×99.3%158 ms195 ms
8 ×99.3%278 ms353 ms
16 ×99.3%525 ms667 ms

The teal line is flat from one bandwidth-delay product onward because it is pinned to the ceiling — the link cannot carry more than the link. Everything spent on memory past that point buys delay at full price and throughput at nothing. And the left end of the same line is why the buffer exists at all: at a quarter of a BDP the flow’s own sawtooth leaves the link idle 11% of the time. There is a correct answer in the middle, it is about one BDP, and it is the classic buffer-sizing rule (B = C × RTT) drawn as the knee of a curve rather than asserted.

The thing to actually watch

Not the numbers — the queue, in the top panel, drawn at its real capacity with the packets that are actually in it. Under drop-tail with a big buffer it never empties. It fills to the brim, a drop finally tells the sender something, the window halves, and the queue drains to half full before climbing again. The link’s memory is doing nothing but adding a constant to every packet’s journey, which is why the phenomenon’s name is a standing queue.

Every packet is one millisecond here (the link runs at exactly one packet per millisecond, 1500 bytes at 12 Mbit/s), so the buffer’s width in packets and its cost in milliseconds are literally the same number. 640 packets of memory is 640 ms of latency, written on the box.

The three fixes, and what each one actually fixes

A delay-based sender (Vegas). Reno’s control signal is loss, and loss is a thing that only happens once the buffer is full — so Reno cannot find the right window without first overrunning whatever memory you gave it. Vegas watches the RTT instead, estimates how many of its own packets are sitting in a queue, and holds that number between 2 and 4. Switch to it and the 640-packet buffer stops mattering: 3 packets queued, 43 ms, 99.3% of the link. The buffer is still there. The sender simply never fills it, which is the whole idea behind every delay- or rate-based controller since, BBR included.

CoDel on the queue. Nichols and Jacobson’s insight was that queue length is the wrong thing to measure — a burst that drains is fine, and a short queue that never drains is not — so CoDel measures how long packets sit and starts dropping once the minimum sojourn stays above 5 ms for a full 100 ms interval. It pins the round trip at 42 ms at every buffer size in this demo. It also costs 13 points of throughput here, and that number is real rather than a bug: a single Reno flow needs about a BDP of slack to cover its own sawtooth, and CoDel by design will not give it one. With dozens of flows whose sawtooths are out of phase, the aggregate keeps the link full and the cost largely vanishes — but with one flow, on one link, you can see exactly what you are trading.

Fair queueing. The one worth sitting with. Give each flow its own lane and round-robin between them, and nothing about the standing queue changes — the bulk flow still rides up to 360 ms, the buffer still fills, the sawtooth is identical. The small flow goes to 40 ms flat, at every buffer size, because its lane was empty and it never had to stand behind anyone. Delay was never a property of the link. It was a property of the queue you were made to share.

FQ-CoDel, which is what actually ships in Linux and in every decent home router now, is simply both switches on at once.

Reuse

src/standing.js is a framework-free ES module with no rendering and no timers:

Gotchas