API Design

Foundations · Lesson 10

SLIs, SLOs & SLAs

"Reliable" is meaningless until you measure it. These three acronyms turn a vague promise into numbers: what you measure (SLI), what you aim for (SLO), and what you promise contractually (SLA). They're how serious teams reason about quality — and a favourite interview topic because they reveal whether you think about operations, not just code.

⏱ 11 minDifficulty: corePrereq: Lesson 04

By the end you'll be able to

💡 Beginner Cheat Sheet: Core Acronyms Explained

Before diving into targets, let's define the fundamental metrics engineers use to describe speed, throughput, and reliability:

Three layers of the same promise

SLI — Indicator what you measure: "99.95% of requests succeeded" SLO — Objective your target: "≥ 99.9% monthly" SLA — Agreement the promise + penalty
A pyramid: you can't promise (SLA) what you don't target (SLO), and you can't target what you don't measure (SLI). Build bottom-up.
✅ The one-line distinction

SLI = the speedometer reading. SLO = the speed limit you set yourself. SLA = the ticket you pay if a cop catches you over a higher line. If you can say that, you understand all three.

Good indicators measure the user's experience

Pick SLIs that reflect what users actually feel, not what's easy to graph. The common ones for an API:

SLIMeasuresTypical phrasing
AvailabilityDid the request succeed at all?% of non-5xx responses
LatencyWas it fast enough?% of requests under 300 ms (use p99, not average — Lesson 04)
Error rateHow often it fails% of requests returning errors
Throughput / correctnessVolume served / right answerrequests/sec; % of correct results
⚠️ Common trap: chasing 100%

"We'll target 100% uptime" is a red flag, not an ambition. The last fraction of a percent costs exponentially more (redundant everything, no risky deploys) and is statistically unprovable. Worse, if users reach you through their own flaky networks, they can't tell the difference between 99.99% and 100% anyway. Good teams pick the lowest reliability target that keeps users happy — and spend the freed-up effort on features.

The error budget: turning an SLO into decisions

Here's the idea that makes SLOs powerful. If your SLO is 99.9% success, then 0.1% failures are allowed — that's your error budget. Over a 30-day month, 99.9% availability permits about 43 minutes of downtime. That budget is a currency you get to spend:

This dissolves the eternal dev-vs-ops fight ("move fast" vs "don't break things") into a shared number. The budget, not opinions, decides whether today is a day for features or for hardening.

Availability SLOAllowed downtime / 30 days
99%~7.2 hours
99.9% ("three nines")~43 minutes
99.99% ("four nines")~4.3 minutes
99.999% ("five nines")~26 seconds
🎯 Interview angle

In any design question, stating a target reframes the whole solution: "I'll target 99.9% availability and p99 latency under 300 ms." That single sentence justifies your redundancy, caching, and retry choices — each is in service of a number, not decoration. And if asked "why not five nines?", answer with cost and the error-budget trade-off. That's senior-level reasoning interviewers specifically look for.

Under the hood: how it actually works

How an SLI is computed from raw events

An SLI is not a single sensor reading — it is a ratio computed over a time window. The canonical formula:

## Availability SLI (most common for APIs)
SLI = good_events / valid_events

## Example over a 1-minute window
valid_events = all HTTP requests (excluding health checks, pre-planned maintenance)
good_events  = requests that returned a non-5xx response within the latency threshold

## Normative API availability SLI (4xx excluded from valid):
## If in one minute: 9,900 × 2xx, 87 × 4xx, 13 × 5xx/timeout
valid_events = 9,900 + 13 = 9,913   # exclude client errors
good_events  = 9,900                # successful server outcomes only
SLI (this minute) = 9900 / 9913 ≈ 0.9987  = 99.87%

## Variant (4xx count as "available" to the server): good=9987, valid=10000
## — only use if you explicitly want that definition; document it.

## The monthly SLI aggregates all 1-minute windows:
SLI (month) = sum(good_events over all windows) / sum(valid_events over all windows)

Critical design decision: what counts as valid and good? Normative for this lesson: client errors (4xx) are excluded from both numerator and denominator — they don't reflect service health. Only 5xx and timeouts count as failures among valid events. Pick one definition and use it in every query and example; mixing “4xx are good” with “exclude 4xx from the denominator” produces ratios above 1.0 and two teams will not implement the same SLI.

Error budget arithmetic — step-by-step guide

The error budget is the allowed failure margin of your system. If your target (SLO) is 99.9% uptime, it means your allowed error rate is 0.1%. Let's calculate exactly what that means in terms of time and requests:

🧮 Step 1: Converting SLO to allowed downtime (Time-based budget)

Let's find out how many minutes our API is allowed to be down over a 30-day month:

  1. Calculate total minutes in 30 days: $$30\text{ days} \times 24\text{ hours/day} \times 60\text{ minutes/hour} = 43,200\text{ minutes}$$
  2. Identify the allowed failure fraction: $$\text{Allowed Failure %} = 100\% - 99.9\% = 0.1\% \implies 0.001$$
  3. Multiply total minutes by the failure fraction: $$\text{Allowed Downtime} = 43,200\text{ minutes} \times 0.001 = 43.2\text{ minutes}$$

This means your service is allowed to be completely down for 43.2 minutes in a month. If it is down for 45 minutes, you have violated your SLO.

🧮 Step 2: Converting SLO to allowed failures (Event-based budget)

Now, let's look at it in terms of actual failed API requests. Suppose your API handles an average of 1,000 requests per minute:

  1. Calculate the total expected requests in a month: $$\text{Total Requests} = 43,200\text{ minutes} \times 1,000\text{ requests/minute} = 43,200,000\text{ requests}$$
  2. Calculate the maximum number of failed requests allowed: $$\text{Allowed Failures} = 43,200,000\text{ requests} \times 0.001 = 43,200\text{ failed requests}$$

This is your monthly Event Error Budget: 43,200 allowed bad requests.

🧮 Step 3: Depleting the budget during an incident

Suppose you experience a major server incident: the database gets locked, and for 10 minutes, all 1,000 requests/minute fail with a 500 error code.

  1. Compute the cost of the outage: $$\text{Outage Failures} = 10\text{ minutes} \times 1,000\text{ requests/minute} = 10,000\text{ failed requests}$$
  2. Determine the percentage of the budget consumed: $$\text{Budget Consumed} = \frac{10,000\text{ outage failures}}{43,200\text{ allowed failures}} \approx 23.1\%$$
  3. Calculate the remaining budget: $$\text{Remaining Failures} = 43,200 - 10,000 = 33,200\text{ failed requests}$$ $$\text{Remaining Minutes} = 43.2\text{ minutes} \times (1 - 0.231) \approx 33.2\text{ minutes of downtime left}$$

In just 10 minutes, you have burned through nearly a quarter (23.1%) of your entire monthly safety budget!

Burn rate alerting

Checking whether the SLO was met at the end of the month is too late. Burn rate alerting fires when you are consuming budget faster than sustainable. The burn rate is how many times faster you are burning the budget compared to the allowed baseline:

## Burn rate = (current error rate) / (SLO error rate)
SLO = 99.9%  → SLO error rate = 0.001 (0.1%)

## If in the last hour the error rate is 1%:
burn_rate = 0.01 / 0.001 = 10x

## At burn rate 10x you exhaust the monthly budget in:
time_to_exhaust = 30 days / 10 = 3 days

## Common alert thresholds (Google SRE Workbook):
##   Page immediately (burn rate ≥ 14.4 → budget gone in <2 hours)
##   Ticket (burn rate ≥ 1 over 6 hours → 5% budget consumed)

Burn rate alerts avoid two failure modes: missing a slow, low-rate degradation that quietly drains the budget, and being flooded with alerts for brief spikes that recover quickly.

Days in month → Budget remaining 100% 50% 0% outage 1 10% consumed outage 2 20% left → alert 0 10 20 30
Error budget for a 99.9% SLO over 30 days. Outages create steep drops; a burn-rate alert fires when the remaining budget hits the threshold line — not at month's end when it is too late to react.

How to debug & inspect it

SLIs live in your metrics system. The queries below use PromQL (Prometheus Query Language) — the most common format — but the logic is identical in Datadog, CloudWatch, or any metrics platform.

# Availability SLI (normative): 2xx|3xx good; 4xx excluded from valid sum(rate(http_requests_total{status=~"2..|3.."}[30m])) / sum(rate(http_requests_total{status!~"4.."}[30m])) ; Do NOT use non-5xx / non-4xx — that counts 4xx as good while dropping them from the denom # p99 latency SLI: 99th percentile request duration over the last 5 minutes histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])) ) # Current burn rate (compare to SLO error rate = 0.001 for 99.9% SLO) 1 - ( sum(rate(http_requests_total{status=~"2..|3.."}[1h])) / sum(rate(http_requests_total{status!~"4.."}[1h])) ) / 0.001 ; Result > 1 → burning faster than the SLO allows ; Result > 14.4 → page immediately (budget exhausted in <2 hours) # Error budget remaining for the month (assumes 30d window, SLO 99.9%) 1 - ( 1 - sum(rate(http_requests_total{status=~"2..|3.."}[30d])) / sum(rate(http_requests_total{status!~"4.."}[30d])) ) / 0.001 ; 1.0 = full budget · 0.0 = exhausted · negative = overdrawn
Symptom / findingLikely causeAction
SLI drops suddenly at a known timeDeployment, config change, or dependency outage triggered at that timeCorrelate with deployment log; roll back or fix-forward; add change markers to dashboards
SLI looks fine but users report slownessSLI measures wrong thing — e.g. using average latency instead of p99; or excluding a subset of usersSwitch to percentile SLI; segment by region, user tier, endpoint; check which requests are slow
Burn rate alert fires but SLI looks normalAlert window is shorter than SLI window; a short spike consumed budget faster than the long average revealsAdd a short-window (5m) SLI chart next to the long-window (30d) one to detect spikes
SLI is 100% but the service is actually downMetrics pipeline itself is broken — if no requests flow, the ratio is undefined or the scraper is offlineAdd a request-rate alert: if rate(http_requests_total[5m]) == 0 during business hours → investigate
Error budget exhausted on day 3 of 30Major outage or high sustained error rateFreeze risky changes; hold incident retro; fix root cause; consider temporary SLO adjustment with stakeholders
SLI is 99.95% but SLA says 99.9% — am I safe?SLI and SLA windows may differ; SLA may exclude maintenance windowsRead the SLA definition carefully; measure your SLI over the exact same window and exclusions the SLA specifies

Checklist for a new SLI:

  1. Define what "valid" means — which requests/events count toward the denominator? (Exclude health checks, pre-planned maintenance, and client errors unless you want to measure them.)
  2. Define what "good" means — non-5xx? Under 300ms? Both? Write the exact metric query before setting a target.
  3. Check the SLI in production before setting the SLO — if current availability is 99.5%, a 99.9% SLO means your error budget is already gone.
  4. Add a rate alert alongside the SLI — if the request rate drops to zero, a perfect SLI is meaningless and potentially masking a total outage.
  5. Review with users: does a p99 latency SLI actually capture what makes them complain, or do you need to segment by endpoint or user tier?
⚠️ The Goodhart's Law trap

Once a metric becomes a target, people optimise for the metric rather than what it was measuring. Common examples: excluding "expected" error codes to hit the SLO number; targeting average latency (which ignores the slow tail that users actually feel); or setting an availability SLO on the load balancer health check instead of the real user flow. Guard against this by validating SLIs against user-reported complaints — the SLI should predict user pain, not just be easy to hit.

By the numbers

Error budgets and burn rates convert abstract SLOs into concrete operational decisions. The governing formulas:

error_budget = 1 − SLO # fraction of failures allowed budget_minutes_30d = (1 − SLO) × 30 × 24 × 60 # allowed downtime-equivalent in a month burn_rate = observed_error_rate / error_budget # how fast you are consuming the budget days_to_exhaust = 30 / burn_rate # days until budget is gone at current rate

Scenario A (matches Step 2 above): checkout API at 1,000 req/min, 99.9% SLO → event budget = 43,200 failures/month. Scenario B: same SLO at 1,000 req/s → event budget = 2,592,000 failures/month. Never mix the two traffic units with one budget number.

Error budget table — SLO targets over 30 days (event column = 1k req/min):

SLO targetError budget (fraction)Allowed downtime / 30 dAllowed failures @ 1k req/min
99%0.01 (1%)~7.2 hours~432,000
99.9% ("three nines")0.001 (0.1%)~43.2 min~43,200
99.95%0.0005 (0.05%)~21.6 min~21,600
99.99% ("four nines")0.0001 (0.01%)~4.3 min~4,320
99.999% ("five nines")0.00001 (0.001%)~26 sec~432

At 1,000 req/s the same fractions apply to downtime, but allowed failures scale ×60 (e.g. 99.9% → 2,592,000 failures/month).

Worked burn-rate trace — 99.9% SLO, 1% observed error rate (rate cancels; units must still match for absolute counts):

SLO = 99.9% → error_budget = 0.001 (0.1% failures allowed) observed_rate = 0.01 (1% of requests are failing right now) # Burn rate (unit-free — same at any QPS): burn_rate = 0.01 / 0.001 = 10× ← consuming budget 10× faster than sustainable days_to_exhaust = 30 / 10 = 3 days ← monthly budget gone in 3 days at sustained 1% # Absolute counts at 1,000 req/s (not 1k req/min): failures_per_hour = 1,000 req/s × 3,600 s × 0.01 = 36,000 failures total_budget = 1,000 × 86,400 × 30 × 0.001 = 2,592,000 failures this month consumed_per_hour = 36,000 / 2,592,000 ≈ 1.39% of monthly event budget per hour # At t=1h into incident (1k req/s): budget_remaining = 2,592,000 − 36,000 = 2,556,000 failures full-outage time left ≈ 2,556,000 / 1,000 ≈ 2,556 s ≈ 42.6 minutes # Same 1% incident at 1,000 req/min (budget 43,200): failures_per_hour = 1,000/min × 60 × 0.01 = 600; 600/43,200 ≈ 1.39%/hour — same burn fraction

Multi-window alert thresholds (following the Google SRE Workbook burn-rate model):

Alert windowBurn rate thresholdBudget consumed before alert firesSeverity
5 min + 1 h≥ 14.4×~2% (fires fast — budget gone in <2 hours)Page immediately
30 min + 6 h≥ 6×~5% (budget exhausted in ~5 days)Page (urgent)
6 h + 3 d≥ 1×~10% (slowly burning — ticket review)Ticket
End-of-month reviewSLO missed100% (too late to react)Post-mortem only
# Worked multi-window example: SLO=99.9%, error_budget=0.001 # Incident fires burn_rate=14.4× for 5 minutes AND 1 hour: burn_rate = 14.4 budget_per_hour = error_budget / 720 # 720 hours in 30 days fraction_consumed_in_1h = burn_rate × (1/720) = 14.4/720 ≈ 2% # → 2% of monthly budget gone in 1 hour; page-level alert fires. # Two windows required (5m and 1h both above threshold) to reduce false positives: # a 5-minute spike at 14.4× alone would only consume 14.4/(720×12) ≈ 0.17% — not critical # but if still above threshold at 1h window, the degradation is real and sustained.

Decision math — SLO target vs release velocity:

Looser SLO → larger error budget → more room to take risks → faster feature velocity Tighter SLO → smaller budget → any incident eats a large fraction → forced reliability focus Example: a 10-minute deploy-caused outage (time-based budget; QPS cancels) at 99.9% SLO (budget = 43.2 min): 10 / 43.2 ≈ 23% of budget → painful; fails a strict <20% burn policy at 99.99% SLO (budget = 4.3 min): 10 / 4.3 ≈ 233% of budget → SLO breach on first outage at 99% SLO (budget = 432 min): 10 / 432 ≈ 2.3% → comfortable headroom Break-even under a <20% burn policy for ~10 min deploy-error/month: 10 / total_budget_minutes < 0.20 → total_budget_minutes > 50 min → need SLO looser than ~99.88%, OR cut deploy-error time (canary/blue-green) 99.9% (43.2 min) does NOT satisfy budget > 50 min: 10 min already burns ~23% To keep 99.9% and <20% burn: drive deploy-caused error minutes below ~8.6 min/month Tighten SLO only after change-fail minutes are reduced.

Sources: Google SRE Workbook — Alerting on SLOs (burn rates); Google SRE Book — Service Level Objectives; Google Cloud — SRE fundamentals.

When not to use this

🧠 Quick check

1. Which is the contractual promise to a customer, with penalties for missing it?

SLA = Agreement: the external contract with consequences. The SLI is the measurement; the SLO is your internal target (usually stricter than the SLA).

2. Why is "target 100% availability" considered a poor goal?

Diminishing returns: each extra nine multiplies cost while delivering imperceptible benefit. Teams pick the lowest target that keeps users happy and spend the rest on features.

3. Your error budget for the month is exhausted. The disciplined response is to:

The error budget governs risk: out of budget means stop taking risks and harden the system. With budget to spare, you're free to move fast.

✍️ Drill: set SLOs for a checkout API

You own the payment-checkout API. Propose two SLIs, sensible SLOs, and how the SLA should relate. Decide first.

Model answer: SLIs: (1) availability = % of checkout requests returning success (non-5xx); (2) latency = % completing under, say, 500 ms at p99. SLOs: availability ≥ 99.95% monthly (payments are high-stakes, so stricter than a typical service), p99 latency ≤ 500 ms for ≥ 99% of requests. SLA: promise customers something looser, e.g. 99.9%, so the gap between SLO (99.95%) and SLA (99.9%) is your safety margin — you'll usually breach your internal target and react long before you owe anyone a refund. Tie it together: the strict SLO justifies redundancy, idempotent retries (Lesson 08), and aggressive monitoring.

Rubric: ✓ user-centric SLIs (success + latency at a percentile) ✓ SLO stricter than SLA with a stated margin ✓ justifies strictness by payment stakes ✓ connects targets to design choices.

Key takeaways

Sources & further reading