Choosing SLIs
A good SLI is a ratio: good events divided by valid events, over a window. The shape is the point — every SLI reads the same way, 0.999 means 99.9% of what users asked for went well, and the ratio composes directly into the error budgets and burn rates of the next two topics.
This topic builds Harborline's two checkout SLIs from telemetry that already exists: the availability SLI from the Chapter 3 request counters, the latency SLI from the histogram instrumented in Chapter 5. No new code ships. The work is deciding what counts as good and what counts as valid, and writing those decisions down.
The Ratio Shape
Good over valid puts every SLI on the same 0-to-1 scale, immune to traffic volume. A 99.9% checkout availability at 3 requests per second on a Tuesday night means exactly the same thing as 99.9% at 300 per second on a summer Saturday. A raw error count of 50 does not — 50 errors is a catastrophe at low traffic and noise at peak, so any threshold on it is wrong at one end of the week.
The Availability SLI
Availability is non-5xx checkout requests divided by all checkout requests, computed from the status-labeled request counters. The word "valid" carries a decision: exclude what the service cannot control. A user's 4xx from a mistyped card number is not a Harborline failure, so 4xx stays out of the bad-event count — and that call is settled explicitly, per status class, in the SLO document rather than argued mid-incident.
# availability SLI as a recording rule: good (2xx/3xx) over valid (non-4xx)
- record: sli:checkout_availability:ratio_rate5m
expr: |
sum(rate(harborline_bookings_total{env="prod",endpoint="/checkout",status=~"2..|3.."}[5m]))
/
sum(rate(harborline_bookings_total{env="prod",endpoint="/checkout",status!~"4.."}[5m]))
The recording rule divides the rate of good checkout requests — 2xx and 3xx — by the rate of all valid ones, with 4xx excluded from both sides, and stores the result as a single pre-computed series. The dashboard, the error-budget math in Topic 51, and the burn-rate alerts in Topic 52 all read this one series instead of re-deriving the ratio four slightly different ways — which is exactly where denominator mismatches breed.
The Latency SLI From the Histogram
The latency SLI is requests under 500 ms divided by all requests, read straight from histogram buckets: the count in the le="0.5" bucket of harborline_checkout_duration_seconds over the total count. This only works because Chapter 5 placed a bucket boundary at exactly 0.5 seconds. A threshold that falls between buckets can only be estimated — histogram_quantile interpolates inside a bucket — so the SLO threshold and the bucket layout are designed together, at instrumentation time.
# latency SLI: fraction of checkouts completing under 500 ms
- record: sli:checkout_latency:ratio_rate5m
expr: |
sum(rate(harborline_checkout_duration_seconds_bucket{env="prod",le="0.5"}[5m]))
/
sum(rate(harborline_checkout_duration_seconds_count{env="prod"}[5m]))
This rule takes the rate of observations that landed in the under-500-ms bucket and divides by the rate of all observations, yielding the fraction of checkouts that met the latency target. Because le="0.5" is an exact bucket boundary, the number is exact, not interpolated — the one property an SLO measurement cannot do without.
Where You Measure: Load Balancer vs Service
The service's own metrics miss the failures where the service never answered: container down, connection refused, timeout at the edge. During those minutes the bookings exporter emits nothing, and a service-side ratio reads as perfect precisely when users see connection errors. The load balancer sees what the user saw, including every request that never reached a healthy backend. That is the measurement point to aim for; Harborline’s edge nginx does not yet export per-status counters, so the recording rule below ships from the service-side counter first, with the LB view as the target state and the gap between the two kept as a diagnostic.
Few SLIs, User-Journey Shaped
One availability SLI and one latency SLI per critical user journey beats twenty per-endpoint SLIs. Each additional SLI dilutes attention, demands its own definition debate, and adds recording rules and alerts somebody must maintain. Checkout comes first because latency there is money; search gets its SLIs later, once the checkout loop demonstrably works. Internal endpoints that no user journey crosses get dashboards, not SLOs.
- Building the SLI from a cause metric — CPU, queue depth, Redis hit rate — instead of a symptom; the Chapter 8 connection-pool ceiling produced healthy CPU on every host while checkouts timed out, and a cause-based SLI would have read 100% throughout.
- Measuring availability only inside the
bookingsservice — when the container is down it exports nothing, numerator and denominator both stop, and the ratio reads as perfect during the exact minutes users got connection errors. - Setting a 500 ms latency SLO when the histogram's nearest buckets are 0.25 s and 1 s — the measurement carries interpolation error you cannot remove without redeploying new bucket boundaries.
- Counting 4xx responses as bad events — one bot storm of invalid card numbers burns the availability budget while the service performs flawlessly, and the team learns to ignore the SLO.
- Averaging latency instead of using the ratio-under-threshold — a 320 ms average is fully compatible with 10% of users waiting 3 seconds; the mean hides exactly the tail the SLO exists to protect.
- Express every SLI as a good/valid ratio in a Prometheus recording rule, so the SLO dashboard, the budget, and the burn-rate alerts all read one pre-computed series instead of four ad-hoc queries.
- Measure the user-facing SLI at the load balancer and keep in-service metrics as the layer beneath it — the LB number is the truth, the service number is the explanation.
- Design histogram buckets around SLO thresholds at instrumentation time — put an exact boundary at 0.5 s because the SLO says 500 ms, not the other way around.
- Write down, per status class, what counts as valid and what counts as good, and version that decision with the SLO — the 4xx argument should happen once, in review, not during an incident.
Knowledge Check
Why does the good/valid ratio shape beat a raw error count as SLI material?
- Ratios are cheaper for Prometheus to evaluate than counts
- The ratio means the same thing at any traffic volume
- Ratios remove the need to define which events count as failures
- Grafana can only draw gauges from values between 0 and 1
What does a load-balancer-side availability SLI catch that a service-side one structurally cannot?
- The minutes when the service was down and exporting no metrics at all
- Requests that were slow but eventually completed successfully
- Whether a 4xx should count as a bad event or not
- The Redis connection-pool saturation behind slow checkouts
Why must a histogram bucket boundary sit exactly at the 500 ms SLO threshold?
- Prometheus rejects histograms whose buckets don't align with recording rules
- Because more bucket boundaries always make every quantile more accurate at no extra storage cost
- Only an exact boundary gives an exact count; anything between buckets is interpolation
- Grafana refuses to plot values that fall between bucket boundaries
What happens if 4xx responses count as bad events in the checkout availability SLI?
- The SLO becomes usefully stricter, catching more real failures
- Cardinality explodes because every status code becomes its own high-churn series in memory
- Client mistakes spend the error budget, and the team learns to distrust the SLO
- Burn-rate alerts stop evaluating because the ratio exceeds 1
You got correct