Skip to content

Repository files navigation

backpressure-lab

A bounded producer/consumer queue you can push past its own capacity, and the three answers to what should happen next.

This is the Go simulation behind pigfox.com/demos/backpressure-lab. It has no HTTP surface, no template and no browser dependency — the transport and the page live in the site, and this package is the part that does the work.

What it is arguing

Every queue between two components has a capacity. The interesting question is never how big it is, it is what happens when it is full, and there are only three answers:

  • Block the producer. Nothing is lost. The producer waits for a slot, so the cost lands on whoever is producing. This is backpressure.
  • Drop the newest. The producer never waits. The arriving message is refused and the backlog stays as stale as it already was.
  • Drop the oldest. The producer never waits. The longest-waiting message is abandoned to make room, so the backlog stays fresh.

Run the same load through each and they diverge within seconds. The measurement that shows it is not the shed count — it is the rate the producer achieved against the rate it was asked for. Under blocking those two numbers come apart, and that gap is the queue reaching back and slowing its own source.

Time is injected, so the tests are deterministic and instant

Nothing in this package calls time.Now or time.Sleep directly. A live session runs on NewRealClock() and paces itself against the wall, so a viewer sees the queue fill in real time. The same code under NewVirtualClock(start) runs a ninety-second session in microseconds and produces the same numbers, because virtual time only moves when every participating goroutine is waiting — and each advance releases exactly one sleeper, the earliest, ties broken by join order.

No test in this package sleeps. The whole suite, including a forty-five second session and a goroutine-leak check, runs in well under a second.

clock := backpressurelab.NewVirtualClock(time.Date(2026, 8, 7, 12, 0, 0, 0, time.UTC))
lab, err := backpressurelab.New(backpressurelab.Config{
    Workers:       1,
    ArrivalRate:   300,
    ServiceTime:   10 * time.Millisecond,
    QueueCapacity: 10,
    Policy:        backpressurelab.PolicyBlock,
    Lifetime:      5 * time.Second,
}, clock)
if err != nil {
    return err
}
defer lab.Close()

reason := lab.Run(context.Background())
s := lab.Snapshot()

Workers sleep, they do not spin

A worker holding a message for its service time is parked on a timer, not looping on a clock read. That is a safety property rather than a style preference: the ceilings here permit thirty-two live workers across a full house of sessions, and thirty-two spinning goroutines would peg every core on the box for as long as anyone kept the page open.

The ceilings are in the package, not at the edge

Normalize is the only door into a Config, and the global worker budget is committed by this package before a single goroutine starts. A limit enforced by the transport is a limit the second transport does not have.

A request above a ceiling is clamped and reported, never refused silently — a simulation quietly running numbers other than the ones on screen is worse than a refusal. Lab.Clamps() is what changed and why.

knob range
workers 1–8 per session, 32 across all sessions
arrival rate 1–5000 / second
service time 1–50 ms
queue capacity 1–10000
session lifetime 1–90 s

What the accounting promises

Every message the producer offered is in exactly one of five states, at every instant:

Offered == Completed + Shed + QueueDepth + InFlight + Abandoned

Abandoned exists so that identity closes when a session is cancelled while a worker holds a message or a producer is mid-stall. A dashboard whose own arithmetic is wrong is worse than one that says nothing.

Percentiles

Latency is measured end to end, from the instant a message arrived to the instant a worker finished with it, into a fixed bucket ladder with no external dependency and no per-sample retention. A reported percentile is the upper bound of the bucket the quantile falls in, so it never understates — and it can sit above the largest sample ever observed. The overflow bucket has no upper bound to report and returns the observed maximum instead.

Relationship to the site

The .go files here are byte-identical to their counterparts in internal/backpressurelab/ in the pigfox2 repository. Two files from that package are absent rather than changed: view.go and view_test.go, which are the site's page model — asset hrefs, control defaults, the stream path — and have nothing to do with the simulation. A diff over the ten files that are here should be empty; if it is not, the site has a private fork of this package and that is a defect.

Running it

make test      # go test -race ./...
make cover     # 100% of statements

License

MIT, Pigfox LLC. See LICENSE.

About

A bounded producer/consumer queue pushed past capacity: block the producer, drop the newest, or drop the oldest. The Go simulation behind pigfox.com/demos/backpressure-lab.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages