The rate-limiter demo is the Go library, running in your tab
Spigot sends the same burst through four Go rate limiters in the browser, so their tradeoffs show up as moving queues instead of promises in a README.
A fixed-window limiter set to 100 requests per second can admit 200 requests across one well-timed boundary. Send 100 just before the counter resets and 100 just after it. Both batches are legal, even though they arrive a few milliseconds apart.
Spigot puts that edge case next to three alternatives: token bucket, leaky bucket, and sliding window. A burst slider feeds identical synthetic request timing into all four, while four queues show what each limiter admits, delays, or rejects. It is a small lab for backend engineers who would rather watch the tradeoff happen than memorize another comparison table.
The other half is a dependency-free Go package. All four implementations sit behind the same
Limiter interface, so an application can choose an algorithm without changing how it asks
whether a request should proceed.
Four different meanings of “100 per second”
A token bucket accumulates permits at a steady rate, up to a capacity. Quiet time saves tokens, so a later burst can spend them immediately. A leaky bucket instead places work in a finite queue and drains it at a constant rate. One rewards idle time with burst capacity; the other turns uneven arrivals into even output, until its queue fills.
Fixed window is just a counter plus a reset time. That makes it cheap and understandable, but creates the boundary trick in the opening example. Spigot's sliding window avoids the hard reset by weighting the previous window according to how much of it still overlaps the current one. Halfway through a one-second window, for example, half of the previous window's count still matters. The estimate changes gradually instead of falling from 100 to zero at one arbitrary instant.
These algorithms do not produce one universal winner. A login endpoint may want a short burst to feel responsive. A downstream service with a strict processing rate may need a queue. The point of the four panels is to make the cost of each choice visible under the same arrivals.
One clock, one implementation
The browser is not running a TypeScript rewrite of the algorithms. I compile the Go package
with GOOS=js GOARCH=wasm, then let the TypeScript interface call that WebAssembly build for
every admission decision. The code shown by the demo and the code installed with go get are
the same code. That adds a more involved build step, but removes a nastier failure mode: a
polished demo teaching behavior that the published package does not have.
There is one design choice that makes the comparison possible. Every limiter receives a
time.Time in Allow instead of calling time.Now() internally. The simulator can therefore
give all four algorithms the exact same timestamp for a request. Tests can also jump directly
to a refill or window boundary without sleeping and hoping the operating-system scheduler
cooperates.
The same rule applies to batches. AllowN(t, n) admits all n requests or none of them, so a
rejected batch never quietly consumes some capacity. The demo's batch button makes that atomic
behavior visible alongside ordinary single requests.
The limitation is deliberate: Spigot generates traffic in one tab. It does not reproduce a distributed fleet racing to update a shared counter, network latency, or a production trace. The Go limiters are safe for concurrent use within a process, but coordinating limits across machines is a separate systems problem. This lab isolates the algorithm before storage and coordination complicate it.
Try it
Open Spigot, raise the burst slider, and watch fixed window near a reset while sliding window carries part of the previous count forward. Then fire an atomic batch and adjust capacity to find where each limiter refuses it. The Go library, tests, and WASM bridge are on GitHub.
This post is part of the build log: every app my automated factory ships gets written up here, honestly. Browse everything at apps.charliekrug.com. Comments are open below.
Loading comments…