Congestion Lab
A packet-by-packet simulator showing why your video call stutters when someone at home starts a download

- congestion control algorithms, one shared sender
- 5
- published results checked before every deploy
- 4
- shareable scenarios, each showing one effect
- 9
- packets simulated in about half a second
- 288k
On this page
The problem
Every internet connection decides, many times a second, how fast it is allowed to send. That decision explains everyday problems: why a video call stutters when someone at home starts a download, or why a distant server feels slow on a fast line. It is usually taught as one picture, a sawtooth line that most people never see being produced.
Four things stay hidden without a simulator. A server 300 ms away recovers from a lost packet six times slower than one 50 ms away, on the same bandwidth. "Slow start" actually doubles speed every round trip. Oversized router buffers add seconds of delay without losing a packet. And sharing a link fairly works only under specific conditions. Congestion Lab is for networking students, engineers who know the words but not the mechanism, and people who teach it.
What I built
Every packet is simulated, and the curves follow
I built a simulator that sends individual packets over a virtual link. They queue at the router, get dropped when it's full, and are acknowledged. The sending speed you see is whatever the algorithm did in response. No curve comes from a formula. A simulator that draws the shape it expects can never surprise you, and the surprises are the point of this project.
Published results decide whether it can ship
A wrong simulator still draws a convincing sawtooth, so I don't trust it by eye. Four independent results from the research literature are checked across ranges of settings, and a failure blocks the deploy. These cover throughput under loss, link use against buffer size, the advantage of nearby servers, and how two senders converge to equal shares. I had two of them passing before building any screen. The rule is that when a check fails, the simulator is wrong, and the tolerance stays where it is.
Five algorithms share one sender
I implemented five congestion control algorithms from their published specifications: Tahoe, Reno/NewReno, CUBIC, Vegas and BBR v1. They all plug into the same sender, so a comparison measures the algorithms and nothing else. On long, fast links every algorithm first looked the same, because loss recovery was the bottleneck. So I added selective acknowledgement, which wasn't in the plan, to make the comparison mean something.
Four charts share one time axis
The main view stacks sending window, router queue, throughput and round-trip time on one time axis. Reading straight down at any moment shows why the sender did what it did. A bufferbloat page runs the same download over a deep buffer and over a queue built to limit delay: throughput barely changes, while delay differs by a factor of ten. After each run, the site writes its findings out in plain sentences in Indonesian or English, with sources.
It shows where the model stops
Some behaviours can't be modelled faithfully here. I list them as gaps and show them wherever they affect a result. One result surprised me. With a simple drop-when-full queue, packet timing sometimes reversed the expected advantage of nearby servers. I moved the fairness checks to a queue that drops at random and kept a test for the surprising effect.
Result
Congestion Lab is live and public, in Indonesian and English. It has nine curated scenarios, each showing one named effect, and every run can be shared by link without an account or server. The simulator handles 288,000 packets in about half a second in a background thread. It uses no simulation or chart library. An automated accessibility audit found zero violations on its five pages at the time, in both languages. I built it in about 10 hours on 6 August 2026.
Under the hoodTechnical detail for engineers
- Discrete-event engine in
lib/sim/(~3,600 lines): virtual clock, priority queue, link serialisation, queue disciplines, sender and receiver, packet ledger. (config, seed) → SimTraceis byte-identical on any machine: integer-nanosecond time, noDate, no unseeded randomness, and equal timestamps broken by an explicit monotonic sequence number.- Packet conservation (
sent = delivered + dropped + in flight) is asserted at every event in every test suite. - The four oracles: Mathis et al. 1997 throughput, utilisation versus buffer size and BDP, Reno's RTT unfairness ratio, and Chiu & Jain 1989 AIMD convergence. Before any UI, Mathis matched within 13% across 5 loss rates × 4 RTTs, and buffer sizing within 1% across 9 buffer sizes.
- Every algorithm implements
onAck,onLoss,onTimeoutandcanSend. There is noif (algorithm === 'cubic')in the sender. A fixed-rate CBR sender serves as a reference. - SACK follows RFC 2018 and RFC 6675, configurable per flow via
sackEnabled, off for Tahoe. - Queue disciplines: DropTail, RED and CoDel. Over deterministic DropTail the Floyd–Jacobson phase effect inverts the RTT bias, so fairness oracles run on RED and the phase effect is asserted in
tests/oracles/rtt-unfairness.test.ts. - A 60-second 100 Mbps run is about half a million packets. Trace hot fields live in typed arrays with side tables and lazy hydration; the simulation runs in a Web Worker with an event budget. Benchmarked at M0: 288k packets in ~0.5 s with flat heap growth.
MODEL_GAPSinlib/sim/engine.ts: no RACK or Tail Loss Probe, no delayed ACK, no HyStart, ACKs never queue or drop, fixed one-MSS segments, and a 200 ms RTO floor (Linux) instead of RFC 6298's 1 s.lib/analysis/insight.tsgenerates the findings sentences in both locales. Fairness uses Jain's index over time and a Chiu-Jain phase diagram.TraceChartmeasures its container and states distances in device pixels, after 11px labels rendered at about 4px on phones inside a fixed 1000-unit viewBox.bund(3.0:1) andsediment(2.9:1) pass contrast for strokes but not prose, so text-safe-inkvariants were added.- Zero axe-core violations across all five routes in both locales at M6. 6 routes × 2 locales, 148 tests in 18 files, ~12,100 lines of TypeScript, 34 commits, three runtime dependencies.
Screenshots
Hiring, or building something similar?
I'm open to full-time roles and select freelance work.