Learn
Lesson · Measuring Systems · Biases a measurement adds

Coordinated Omission

A closed-loop load generator sends its next request when the last one comes back, so while the system is stalled it sends nothing and the stall is recorded once instead of once per request it delayed. Putting back the requests the schedule called for, each waiting from the moment it was due, moves the p99 of one trace from 2 ms to 990 ms — a factor of 495 — without measuring anything new.

closed loop: the next request goes out when the last one returns a 1 000 ms stall on a 10 ms schedule hides 99 requests naive p99 = 2 ms corrected p99 = 990 ms ×495 the k-th missing request waited stall − k·interval the correction adds no measurement: one stall, charged to all it delayed
One stall, measured once or measured properly

Core ideas

What this lesson establishes

The bias is in the generator, not in the system, and it is invisible in the output because the numbers it reports are all true.

1

A closed loop stops generating exactly when the system stops responding

Send a request, wait for the reply, send the next. It is the obvious way to write a load generator and it has a built-in feedback: the slower the system, the less load it receives. During a one-second stall a loop scheduled at 10 ms apart issues no requests at all, so instead of a hundred slow requests the trace contains one, and the other ninety-nine are simply absent from the sample.

2

Real arrivals do not wait for you to be ready

Users, upstream services and message queues keep arriving during a stall. The requests the closed loop did not send are requests that would have existed, and each would have waited from the moment it was due until the stall cleared. That is what the correction restores: not an invented distribution, but the schedule the generator itself was configured with.

3

The correction changes the sample, never a measurement

Every latency the generator recorded stays exactly as recorded. What is added is one entry per request the schedule called for during the stall, at the wait that request would have experienced. The percentile is then the same nearest-rank rule applied to a larger sample — the disagreement between 2 ms and 990 ms is a disagreement about which requests existed, not about what a percentile is.

The material

The requests that were never sent, and where they belong in the sample

Why a stall appears once in a closed-loop trace, what the imputed latencies are, and the arrival model each of the two answers assumes.

Definition · Coordinated omission

Coordinated omission is the bias in which a measurement process suspends its own sampling during the events it exists to measure. In a closed-loop load generator, the next request is issued only after the previous reply, so a stall of duration S on a schedule of one request every I suppresses about S/I requests — and every one of them would have been slow.

The name says the important part: the omission is coordinated with the fault. Requests are not dropped at random from the sample, which would be harmless; they are dropped precisely when the system is behaving worst, which is where the percentile you are reporting lives.

Consider a generator configured to send one request every 10 ms with a healthy service time of 2 ms. It sends a hundred requests and during one of them the service stalls for a full second. The trace it produces is ninety-nine requests at 2 ms and one at 1 000 ms.

trace as recorded 99 × 2 ms 1 × 1 000 ms n = 100 naive p99 rank ⌈0.99×100⌉ = 99 of 100 → 2 ms schedule during the stall: one request due every 10 ms requests due inside 1 000 ms, not sent: 99 the k-th of them was due at k·10 ms into the stall and waited until the stall cleared: 1 000 − 10k ms k = 1 → 990 ms k = 50 → 500 ms k = 99 → 10 ms corrected sample 99 × 2 ms + 990, 980, …, 10 + 1 × 1 000 ms n = 199 corrected p99 rank ⌈0.99×199⌉ = 198 of 199 → 990 ms 990 ÷ 2 = 495

The naive p99 is 2 ms, which is the healthy service time. That is not a coincidence and it is the signature of the bug: with one slow request in a hundred, rank 99 never reaches it, so the reported 99th percentile is a measurement of the system when nothing was wrong. A report of “p99 = 2 ms” on a run that contained a one-second outage is arithmetically correct and completely uninformative.

The median moves too, and the direction is worth noticing: 2 ms naive against 10 ms corrected. A bias that is usually described as a tail problem reaches the middle of the distribution as soon as the stall is long relative to the schedule.

Theorem · What the correction assumes

Imputing the missing requests at their scheduled times is exactly right when arrivals are open: generated by a process indifferent to how long the system is taking. It is exactly wrong when arrivals are genuinely closed — a fixed population of clients that each wait for their own reply before acting again, such as a bounded thread pool or a human at a terminal.

In the closed case the requests really were not made, the users really did wait once, and the naive number is what happened. Applying the correction there double-counts. Deciding which model your workload follows is a modelling judgement the arithmetic cannot make for you, and it is the assumption this page rests on.

Why more threads is not the fix

The usual first reaction is to raise the generator’s concurrency: a hundred threads instead of one, so a stall blocks only one of them. It moves the number and it does not remove the bias. Each thread is still a closed loop, each still stops issuing during a stall, and a fault that affects the whole service stalls all of them at once — which is the case you are trying to measure. What removes the bias is generating requests on a clock rather than on replies, and recording each request’s latency from its scheduled start rather than from when a thread got round to sending it.

Example · Two generators, one service, one stall

A clock-driven generator issues a request every 10 ms whether or not the last one returned, and timestamps each from its due time. Through the same one-second stall it records a hundred slow requests with waits from 1 000 ms down to 10 ms, and its p99 is 990 ms with no correction applied at all.

The closed-loop generator records one. The corrected sample above is an after-the-fact reconstruction of what the clock-driven generator would have seen, which is why it lands on the same number: the correction is not a heuristic, it is the missing half of the experiment.

The same bias outside load testing

A monitoring agent that scrapes its own metrics and blocks while the process is stalled omits exactly the samples that matter. So does a client library that records latency only for requests that completed and drops the ones the user abandoned, and so does any dashboard whose denominator quietly excludes timeouts. In each case the sample is missing the events the measurement exists to detect, and in each case the reported number is a true statement about a sample nobody wanted.

The check is one question: could this measurement have been prevented from being taken by the very fault it is supposed to record? If yes, the number is biased in a direction that flatters, and the size of the bias is what the lab computes.

Interactive

The requests the load generator never sent

A stall measured once, and the same stall charged to everyone it delayed

Coordinated omissionthe requests a stalled generator never sent, put back at the times they were due
requests that were sentthe stallimputed requests

Method

Correcting a trace for the requests it does not contain

Before any arithmetic, decide whether the correction applies at all. Steps two to four are mechanical once it does.

1

Decide whether your arrivals are open or closed

Open means the request rate is set by something outside the system — users, a queue, a cron fleet — and does not slow down when the system does. Closed means a fixed population each waiting on its own reply. The correction belongs to the open case only, and applying it to a closed workload overstates the tail.

2

Find the intended schedule

One request every I, or whatever rate the generator was configured for. This is the piece of information that makes the correction possible: it is not inferred from the trace, it is what you told the generator to do.

3

Count the requests due inside each stall, and give each its wait

A stall of S starting when a request was due suppresses ⌈S/I⌉ − 1 later requests. The k-th is due k·I into the stall and waits S − k·I until it clears. Add those latencies to the sample; change nothing that was measured.

4

Recompute the percentile on the enlarged sample

Same nearest-rank rule, larger n, so the rank moves too. Report both numbers and the ratio, because the ratio is the finding: a factor of 495 is a statement about the measurement apparatus that the corrected number alone does not convey.

Stalls, schedules and the sample

Score 0 / 0

A hundred requests, a one-second stall, and a p99 of 2 ms

The trace is the lab’s preset: a generator issuing one request every 10 ms against a service that normally answers in 2 ms, through a single stall of one second. Every number below comes from that one trace and the schedule it was generated from.

configured one request every I = 10 ms healthy service 2 ms observed 99 requests at 2 ms, 1 request at 1 000 ms n = 100 AS MEASURED p50 rank ⌈0.50×100⌉ = 50 → 2 ms p99 rank ⌈0.99×100⌉ = 99 → 2 ms p100 → 1 000 ms WHAT THE SCHEDULE CALLED FOR requests due during the 1 000 ms stall, never sent: 99 the k-th was due at 10k ms in, and waited 1 000 − 10k ms k = 1 → 990 ms k = 2 → 980 ms … k = 99 → 10 ms CORRECTED SAMPLE n = 100 + 99 = 199 99 × 2 ms + {10, 20, …, 990} + 1 × 1 000 ms sorted: positions 1– 99 = 2 ms positions 100–198 = 10, 20, …, 990 ms position 199 = 1 000 ms p50 rank ⌈0.50×199⌉ = 100 → 10 ms p99 rank ⌈0.99×199⌉ = 198 → 990 ms 990 ÷ 2 = 495

Nothing in the corrected sample was measured twice and nothing was invented. The ninety-nine added entries are the schedule, and the schedule was an input to the experiment. If you object to them you are objecting to the configured request rate, not to the arithmetic.

The factor of 495 is not a general constant. It is S/I scaled by where the ranks land: lengthen the stall and it grows, shorten the scheduled interval and it grows, and take the stall below one interval and it collapses to 1 because no request was due inside it. The lab lets you find the point where the two columns separate.

For faded practice, set the stall to 200 ms and the interval to 50 ms on the same hundred requests. The supplied first move is that three requests were due inside the stall, at 150, 100 and 50 ms of waiting. Predict both the naive and the corrected p99 before you touch the sliders, then check, and say in one sentence why this trace is so much less alarming than the first.

Common mistakes

1
Believing the generator saw what users would have seenIt saw what a client that politely stops sending during an outage would have seen, which is nobody. The tell is a reported percentile equal to the healthy service time on a run that contained a visible stall: if the p99 and the median are the same number and the maximum is three orders of magnitude away, the sample is missing the requests that were due while the maximum was happening.
2
Fixing it by adding threads or by discarding the outlierBoth make the number prettier. Threads spread the omission across more loops without removing it, and dropping the single slow request removes the only surviving evidence that the stall occurred. The measurement problem is that ninety-nine slow requests are missing, and neither move puts one back.
3
Applying the correction to a genuinely closed workloadA fixed pool of clients that each wait really did experience one delay each, and imputing extra arrivals on top invents load that could not have existed. The correction is a model of open arrivals, it is stated as one, and reporting a corrected percentile without saying which arrival model it assumes is the same failure in the other direction.

Check your understanding

✓
Finish when “could this fault have suppressed its own measurement?” is a question you ask of every measurement.

You should be able to recognise a closed-loop trace from its percentiles, count the requests a stall suppressed from the schedule, give each its imputed wait, recompute the percentile on the enlarged sample, and state whether the workload being measured is open or closed.

Note: The stall in that trace was at least recorded once. A metrics pipeline has a second way of not seeing an event, which is that nothing was sampled while it was happening at all: “Scrape Intervals and What They Cannot See” computes the probability that a spike lands between two reads, and names the longest spike that can hide entirely.

Kept in this browser only, for you. Nothing is sent anywhere and nothing checks it. Carry it to another device.

Feedback and recommendations

Something to change about this lesson? Write it down, tick it off when it is handled, and export the list when you want to act on it. Kept in this browser only: nothing is sent anywhere, and nobody is notified.

No recommendations yet for this lesson.