Part IV · Chapter 7 · When It Breaks · lesson 3
The thing that broke is downstream of the thing that caused it
13 min
Builds on One slow service took down four that do not depend on it
Part IV · Chapter 7 · When It Breaks · lesson 3
13 min
Builds on One slow service took down four that do not depend on it
09:14. The page says pgbouncer: no more connections available. Every checkout
is timing out. The database dashboard is solid red: connections pinned at the
pool ceiling, waiting clients climbing, query latency up tenfold.
Four engineers spend forty minutes on the database. They raise the pool size twice; both times it is full again within a minute.
The database was fine. It is still fine. Whatever caused this had already happened before the first graph moved.
Predict
A cascade has three parts and they are rarely in the same service.
The cause is the change: a deploy, a config push, a key that stopped matching, a certificate that expired, a traffic shift. It is usually small and usually silent.
The amplifier is whatever turned a small change into a large one: a retry loop, an unbounded queue, a pool that makes callers wait rather than refusing them, a cache that stopped absorbing load.
The symptom is what alerted. It is the most downstream thing in the path, because every stage between the cause and it added magnitude.
Incident reviews that name only the symptom produce action items about the symptom, which is how the same cause returns in six weeks wearing a different component's name.
Before you know which service is involved, the shape of the first graph to move tells you what category of thing happened. These are the four worth being able to name on sight.
| Shape | Reading | What it rules out |
|---|---|---|
| Latency rising, completions flat or falling | Saturation. Work arrives faster than it leaves and the difference is sitting in a queue. | A pure demand increase. If demand were the story, completions would be rising too. |
| Latency rising, completions rising with it | Demand. More work arrived and the system is doing more work. | A fault. Go and find out what changed about the traffic before touching the service. |
| Errors rising, latency flat or falling | Fast rejection. Something stopped doing work: a breaker opened, a pool is refusing, a limiter is shedding, a dependency is returning an error quickly. | Saturation. A saturated component gets slower before it gets wrong; a rejecting one gets faster. |
| A step at a timestamp, no ramp | A discrete event. A deploy, a config push, a scheduled job, a leader election, an expiry. | Anything load-driven. Load moves in ramps and cycles; it does not move in a single vertical line. |
The first one always means saturation. It is not a heuristic. Latency rising while completed work stays flat is the definition of a queue growing: arrivals exceed departures, and the gap accumulates as waiting time. Something in the path has run out of a resource and the rest of the system cannot tell which, because waiting looks identical from the outside no matter what is being waited for.
Two consequences follow, and both are commonly missed.
The saturated resource is often not CPU, so CPU utilisation can sit at a third of the box while the service is fully saturated on something else — a connection pool with every connection checked out, a thread pool with every worker blocked, a semaphore, a file descriptor limit, a lock. "Utilisation is low, so we have headroom" is wrong whenever the thing that ran out is countable rather than proportional.
And the saturated component is not necessarily the component whose latency rose. Waiting propagates upward. Every caller of a saturated thing shows the same shape, which is why the shape tells you a queue exists but not where it is.
Case file
Composite, not a real person
A routine deploy at 08:58 adds a currency field to the pricing cache key, so every key written by the new code misses every key written by the old. The rollout is gradual, two instances at a time, over about twelve minutes.
Hit ratio recovered as the old keys were repopulated; connection pressure cleared about four minutes after the rollback, before anybody touched the pool settings again. Total customer impact was fifty-two minutes, of which the first thirteen were the cache warming down and the remaining thirty-nine were spent on a database that was never faulty. The pool was left at 400 for two weeks and caused a separate, smaller incident.
The path was: cache key change, then cache misses, then every request falling through to the database, then connection demand multiplying, then the pool exhausting, then checkout timing out. Six stages. The alert fired on the fifth.
Walking it backwards is mechanical if you ask one question at each stage: what would have to be true upstream for this to look like this? A pool exhausts because demand for connections rose or because connections stopped being returned. Demand for connections rose because queries rose. Queries rose because something that was absorbing them stopped. That is three questions and it lands on the cache without knowing anything about the deploy.
The shape would have said the same thing. Checkout latency rose while completed checkouts stayed flat — saturation, which rules out a demand story, which means the traffic was unchanged and something internal had run out. Nobody looked, because the alert had already named a component.
Check
Most dashboards are organised by component because that is how the exporters are organised. The ordering that actually helps during an incident is by question.
Panel one: arrivals and completions on the same axes. Requests started per second and requests finished per second. When they separate, you have the saturation shape, and you have it before any latency percentile has moved far enough to alert. This single panel distinguishes the first two shapes in the table and almost nobody has it.
Panel two: latency percentiles and error rate on the same time axis. Not the same axes — the same axis of time, side by side, so the direction they move relative to each other is readable at a glance. That distinguishes saturation from rejection.
Panel three: a deploy and config-change annotation overlay. Steps need a changelog next to them, and an annotation stream from the deploy pipeline turns "something changed at 09:58" into a link to a diff. Grafana annotations and the equivalent event overlays in Datadog and in OpenTelemetry-backed stacks all do this, and the work is a webhook from the deploy pipeline.
The methods behind this are worth reading in full: the four golden signals (latency, traffic, errors, saturation) from Google's SRE book, Brendan Gregg's USE method for resources, and Tom Wilkie's RED method for request-driven services. They disagree about vocabulary and agree about the panels.
The whole method is time ordering, so it dies the moment the resolution of your metrics is coarser than the cascade.
A cascade that completes in forty seconds, read off a dashboard scraping every fifteen seconds and graphed as a rate over a five-minute window, shows every signal turning at once. The averaging window smears the ordering out of existence and the earliest departure is no longer visible, so the only thing left to sort by is magnitude — which puts you back on the symptom.
The test is concrete: compare the duration of the incident with the width of the window your panels average over. If the incident is not several times longer than the window, the ordering on the screen is an artefact and you should go to raw event data — request logs with timestamps, the deploy pipeline's own event stream, distributed traces with their span start times — instead of to the graphs. Traces in particular keep per-request ordering that no aggregated metric can reconstruct, and they are the right tool for a cascade that was over before the second scrape.