Why open models and coordinated omission matter

The problem

A classic load generator runs N virtual users in a loop: send a request, wait for the response, send the next one. When the server stalls, every virtual user is stuck waiting. The tool silently stops sending requests, so the stall is represented by only N slow samples, while the thousands of requests that should have been sent during the stall are never issued and never measured. Percentiles computed from that data look healthy. Gil Tene called this coordinated omission: the measured system and the measuring tool coordinate to omit the bad data.

Real users do not wait politely. If your API freezes for two seconds, new visitors keep arriving at the rate they always did, and each of them experiences the freeze.

What loadsmith does about it

  1. Open model (the default). Journeys are started on a schedule of intended start times drawn from a Poisson process (or evenly spaced with arrivals: constant). The schedule does not care whether earlier journeys have finished. Ramp stages are handled as a non-homogeneous Poisson process (see design.md).
  2. Two latencies per request. - service latency: from the moment the request was actually sent to the end of the response. This is what a naive tool reports. - corrected latency: from the request's intended start to the end of the response. If the tool itself could not send on time (concurrency cap reached, event loop saturated), the waiting time is charged to the request.
  3. Both are stored in separate histograms for every step and every second, and both appear in the console, JSON and HTML outputs. SLOs use corrected latency unless you ask for p99:service.

Where intended start applies

max_concurrency and unbounded open models

The cap bounds the number of simultaneously active journeys per run (split across workers). Arrivals above the cap wait, and that wait shows up in corrected latency. Without a cap an open model launches every arrival immediately, so a stall is visible in both views (all those requests really were sent and really were slow) at the price of an unbounded number of connections. The cap models a realistic server-side or client-side limit; pick one close to what your production tier allows.

Worked example

examples/target_server.py freezes every request for 2 seconds out of each 15. examples/shop.yaml offers 40 journeys/s for 15 seconds with at most 5 active journeys, so one stall window falls inside the run.

What happens at the stall:

Result of a real run (results/shop.json, also in examples/sample-results.json):

view p50 p95 p99 p99.9 max
service (naive) 29.9ms 79.1ms 162ms 2.03s 2.05s
corrected 32.6ms 1.48s 1.89s 2.03s 2.05s

The naive p99 says "162 ms, fine"; the SLO p99 < 500ms on corrected latency fails with 1.89 s, which is what the people who arrived during those two seconds actually saw. Per step, login (the first request of every journey) carries the damage: p95 1.85 s corrected versus a service p99 of 215 ms.

The deterministic version of this experiment is tests/test_coordinated_omission.py: a fake clock and a fake transport with a stall window, run through the real engine, asserting service p99 < 50 ms and corrected p99 > 1.5 s.

Practical advice