Symptom-based alerting: the six signals tied to user impact
coreintermediateA symptom is something a user (or a downstream system acting on their behalf) actually experiences — a failed request, a slow page, a stale dashboard. An alert should fire on symptoms, not on whatever happens to be easy to measure inside a server. The roadmap names six symptom signals that cover most systems: error rate (the fraction of requests failing), tail latency (p95/p99, not the average, since the average hides exactly the slow requests users notice), saturation (how close a resource is to its limit — queue depth, connection pool usage, thread pool occupancy), queue age (how long the oldest unprocessed item has been waiting, which is what a user or downstream consumer actually feels), data freshness (how stale a cache, replica, or materialized view is relative to the source of truth), and availability (whether the service can be reached and used at all, as an end-to-end measurement). Each of these maps to something a real person or dependent service notices going wrong — which is the entire design principle.
Think of it as
Think of a car dashboard built for the driver, not the mechanic. The driver needs to know "the engine is overheating" (a symptom that affects whether the car can be driven right now) — not "cylinder 3 coolant sensor reads 4.7 ohms" (a raw internal reading that may or may not mean anything is actually wrong). A mechanic cares about the sensor reading during diagnosis; the driver needs the dashboard to escalate only when something changes what they can actually do with the car. Symptom-based alerts are the dashboard warning lights; raw infrastructure metrics are the sensor readings a mechanic pulls up after the light is already on.
What we're doing: Turn a vague "checkout feels slow sometimes" complaint into symptom-based alert coverage.
- 2
- Error rate is a ratio over a rolling window, not a raw error count — the same absolute count of errors means something different at 100 req/s versus 10,000 req/s.
- 5
- p99, not the average — a small but real fraction of checkouts hanging is exactly what an average latency metric hides.
- 8
- Queue age catches a failure mode neither error rate nor latency sees: individual requests can be fast and successful while a backlog quietly grows behind them.
Why this works: The vague complaint "checkout feels slow" is actually three separate, independently-alertable symptoms — outright failures, individually slow requests, and a growing backlog — and each needs its own threshold because they can fail independently of one another.
Setting one alert threshold for "errors" that conflates hard failures with soft degradation
Wrong
Better
What you see: A raw error count of "10" pages the team identically whether it happened during 50 requests (a 20% failure rate — real outage) or 500,000 requests (a 0.002% failure rate — background noise) — the on-call engineer cannot tell severity from the alert itself and has to go look every time.
Why: A count has no denominator, so it cannot express rate, and it conflates two different situations that need different responses: a small number of slow-but-successful requests is degradation worth a ticket, while a spike in outright failures is an outage worth a page. Splitting the threshold by symptom, and expressing error alerts as a rate rather than a count, lets severity be read directly off which alert fired.
- Error rate — requests failing
- Tail latency — p95/p99, not mean
- Saturation — resource near limit
- Queue age — oldest item waits
- Data freshness — staleness vs source
- Availability — reachable end-to-end
The six symptom signals and what a user actually feels when each fires
Remember: Alert on what a user or dependent system actually experiences — error rate, tail latency (p95/p99, never the average), saturation, queue age, data freshness, and availability — not on whichever internal metric happens to be easy to scrape. A symptom alert tells the on-call engineer that something is actually wrong for someone; everything else is diagnostic detail that belongs in a runbook link, not a separate page.

