Four Signals That Matter: Choosing SLIs Users Actually Feel
Most SLI dashboards track things nobody notices. Here's how we picked the handful of signals that map to real user pain, and dropped the vanity metrics.
Key takeaways
- Most SLI dashboards track things nobody notices.
- Here's how we picked the handful of signals that map to real user pain, and dropped the vanity metrics.
Four Signals That Matter: Choosing SLIs Users Actually Feel#
We had 214 alerts wired into PagerDuty and a checkout flow that still felt slow to customers. During one incident review a support lead pulled up a recording of a user rage-clicking the "Place order" button while every dashboard in the room glowed green. CPU was fine, memory was fine, the pods weren't restarting. The thing users touched was broken and nothing we measured said so.
That mismatch is the whole problem. You can watch a hundred host-level metrics and still miss the one number a person experiences. So we tore the dashboards down and rebuilt around signals a user would actually feel if they degraded.
Start from the request, not the host#
The trick that changed things for us: for every SLI, finish this sentence out loud. "If this gets worse, a user will notice because ___." If you can't finish it, it's not an SLI, it's a diagnostic. Diagnostics are useful during an incident. They don't belong on the reliability scorecard.
CPU at 80% doesn't finish the sentence. A user cannot feel CPU. Checkout p99 crossing 2 seconds does finish it, they feel the spinner. That single filter cut our candidate list from roughly forty metrics to four categories.
Latency, measured where the pain is#
Latency is the obvious one and the most commonly botched. Two mistakes we made and fixed:
We averaged. Averages hide the tail where users leave. Switch to percentiles and pick the one that matches the journey. For a search box, p99 matters because a slow tail feels janky. For a nightly export, p50 is enough.
We measured server time, not the time the user waits. The gap between them is queueing, and queueing is exactly what spikes under load. We now measure at the load balancer, not inside the app.
Here's the histogram query we run for the checkout submit endpoint:
histogram_quantile(
0.99,
sum by (le) (
rate(http_request_duration_seconds_bucket{
route="/api/checkout/submit", method="POST"
}[5m])
)
)
Our target: p99 under 800ms, measured over a 5-minute window. We picked 800ms because internal testing showed abandonment climbing sharply past a full second, and we wanted headroom.
Errors that reached the user#
Not every 500 in your logs hurt someone. A background retry that failed and then succeeded is noise. What counts is the error the user saw. We split error rate by whether the request was user-facing and whether it was retried.
sum(rate(http_requests_total{route=~"/api/.*", code=~"5..", retried="false"}[5m]))
/
sum(rate(http_requests_total{route=~"/api/.*"}[5m]))
The retried="false" label is the important part. Adding it dropped our reported error rate from 0.9% to 0.11%, because most of that 0.9% was retried and invisible to customers. Chasing the inflated number would have burned a sprint on nothing.
Saturation, but only the resource that gates users#
Saturation is where teams over-instrument. You do not need every queue depth on the wall. You need the one resource that, when full, makes users wait. For us that was the database connection pool. When it saturated, requests queued, and latency spiked, and the chain of pain started there.
max_over_time(pgbouncer_pools_cl_waiting[1m]) > 5
Any waiting clients on PgBouncer meant requests were parked. That single gauge predicted latency incidents about ten minutes ahead of the latency SLI itself, which made it our best leading indicator.
Freshness, the signal people forget#
For anything data-driven, an analytics feed, a recommendation model, a replicated read store, "is it correct" is really "is it recent." Stale data returns HTTP 200 and looks perfectly healthy. Users notice when yesterday's inventory tells them something is in stock and it isn't.
We track lag directly:
SELECT EXTRACT(EPOCH FROM (now() - max(event_time))) AS lag_seconds
FROM analytics.orders_replica;
Alert if lag_seconds goes past 300. Green pipelines with three-hour-old data caused two of our worst customer complaints last year, and no latency or error metric would ever have caught them.
Wiring these into an SLO you can defend#
Four signals, one target each, and a burn-rate alert instead of a static threshold. Static thresholds page you at 3am for a blip. Burn rate pages you when you're actually spending your error budget fast enough to run out.
- alert: CheckoutLatencyBurn
expr: |
slo:latency_error_ratio:rate1h{service="checkout"} > (14.4 * 0.001)
and
slo:latency_error_ratio:rate5m{service="checkout"} > (14.4 * 0.001)
for: 2m
labels:
severity: page
The 14.4 multiplier against a 0.1% budget is the standard fast-burn window, it fires when you'd exhaust a month's budget in about two days.
The call we'd make#
If you're starting over, resist the urge to keep the old dashboards "just in case." Pick latency, user-visible errors, the one gating resource, and freshness. Wire burn-rate alerts on those four and page on nothing else. Everything the old wall-of-graphs gave us, we kept as diagnostics you open during an incident, not as things that wake someone. Our page volume dropped from 214 alerts to 9, and the nine actually meant something.
For the more general audit process behind picking signals like these — including how we test whether a candidate SLI actually correlates with user pain — see SLI design: the five-step audit for picking metrics.
Get the DevOps Troubleshooting Cheat Sheet
Subscribe and get our free one-page reference for the errors that eat an afternoon — CrashLoopBackOff, OOMKilled, Terraform state locks, and more — plus new guides as we publish them.
GitHub Actions Reusable Workflows: DRY Pipelines at Org Scale
We had the same 180-line build workflow copy-pasted into 60 repos. Fixing one bug meant 60 PRs. Here's the reusable-workflow setup that made it one.
Blameless Postmortems: The Template and Facilitation That Works
Our early postmortems quietly assigned blame and taught people to hide mistakes. Here's the template and the facilitation rules that finally made them honest and useful.
More from DevOps
Explore more articles in this category
GitLab's New Rate Limits: What to Fix Before Oct 19
GitLab is capping unauthenticated API calls at 60 an hour starting October 19, and the preview windows land before most teams will have noticed.
Storm-3068: A CI/CD Pipeline Is a Kubeconfig Exfiltration Machine
Microsoft's Storm-3068 report used zero malware to steal Kubernetes credentials, just a password reset, a pipeline edit, and permissions nobody had scoped down.
Docker Cloud Sandboxes: Why Agents Need MicroVMs, Not Containers
Docker put AI coding agents in hosted microVMs instead of containers, because a container was never the isolation boundary this job needed.
You might have missed
Evergreen posts worth revisiting.