Throughout the module we have been postponing one decision: what "going well" means. We have metrics (06-01), traces (06-02), resilience patterns (06-03) and autoscaling (06-04), but nobody has yet said how many errors on POST /v1/orders are acceptable, how long the saga may take before someone must look, or whose phone rings when the DLQ fills up at three in the morning. Without those answers, observability is a collection of pretty dashboards consulted after the disaster. This lesson turns it into an operating system: SLOs that translate the customer's experience into numbers, alerts that fire only when those numbers are at risk, on-call rotations and runbooks that know what to do, and a way to manage incidents and learn from them without looking for someone to blame.
Contents
- SLI, SLO and SLA: definitions and error budget
- Choosing TechCorp's SLIs
- Concrete SLOs and their monthly budget
- Expressing the SLIs in PromQL with recording rules
- SLO-based alerts: multi-window burn rate
- Symptom-based alerts, and why not to alert on causes
- Alertmanager: routes per team, grouping, silences and inhibition
- On-call and runbooks
- Incident management: roles, severities and communication
- Blameless postmortem: INC-2031
- SLO review and error budget policy
- Summary table of TechCorp's alerts
- SLI, SLO and SLA: definitions and error budget
| Term | What it is | Who sets it | TechCorp example |
|---|---|---|---|
| SLI (Service Level Indicator) | A measurement of service quality, normally a proportion of good events over the total | Engineering | Proportion of POST /v1/orders that do not return 5xx |
| SLO (Service Level Objective) | An internal target on an SLI over a window | Engineering + product | 99.9% of POST /v1/orders without 5xx over 30 days |
| SLA (Service Level Agreement) | A contract with consequences (penalties) if breached | Business/legal | "99.5% monthly API availability" with the marketplaces that integrate TechCorp |
Rules: the SLA is always looser than the SLO (if the SLO is 99.9%, the SLA 99.5%: the margin is what avoids paying penalties); an SLI is expressed as a proportion (0-1) so it can be summed, compared and turned into budgets; and a 100% SLO does not exist: each additional nine multiplies the cost and no dependency (the cloud, the PSP) offers it.
The error budget is the part of the SLO that is allowed to fail: 1 − SLO. With a 99.9% SLO over 30 days, 0.1% of requests may fail; expressed in time, if the service were completely down, 43 minutes per month. It is the most useful idea in the lesson: it turns "reliability" into a currency that is spent on deployments, experiments and accidents, and that, when exhausted, forces you to stop (section 11).
- Choosing TechCorp's SLIs
A good SLI measures what the customer perceives, not what is convenient for engineering. Ana Ruiz does not know what CPU is; she knows whether her order was created, whether the products page loads fast and whether the confirmation reaches her. TechCorp's four SLIs, one per question:
| SLI | Customer's question | Definition (good / total) | Source metric (06-01) | Owning team |
|---|---|---|---|---|
| Orders availability | "Can I buy?" | POST /v1/orders with code ≠ 5xx / all POST /v1/orders |
http_requests_total measured at the gateway |
Orders |
| Catalog latency | "Does the store load fast?" | GET /v1/products in < 300 ms / all |
http_request_duration_seconds_bucket{le="0.3"} |
Shopping Experience |
| Saga on time | "Do they confirm my order right away?" | Orders confirmed in < 60 s from creation / orders that reach a resolution | saga_duration_seconds_bucket{le="60"} |
Orders (and Payments, Inventory) |
| Outbox freshness | (internal: predicts the previous one) | Instants when outbox_pending < 100 / instants measured |
outbox_pending |
Orders |
Design notes:
- The availability SLI is measured at the gateway (what the customer sees), not at Orders: if Orders is healthy but the gateway does not route, the customer suffers all the same. 4xx do not count as failures: a 422 for invalid data is the client's fault; a 503 because Catalog is down (06-03) does count, even if it is "someone else's".
- The latency one is defined as a proportion of requests under a threshold, not as "the p95 < 300 ms". The proportion is a clean SLI (good/total) and directly exploits the histogram's
le="0.3"bucket; the p95 is still watched in Grafana. - The saga one needs the histogram from 06-01 and excludes from the denominator the orders cancelled for
OUT_OF_STOCKorPAYMENT_REJECTED(resolving quickly with a rejection is also "on time":saga_duration_secondsis observed when the order closes in any final status). - The outbox one is an internal SLI: the customer does not see it, but when it fails the saga one will fail minutes later. A good SLO system has few customer-facing SLIs and some internal ones that warn earlier.
- Concrete SLOs and their monthly budget
| SLO | Target | Window | Error budget | In minutes of "total outage" per month |
|---|---|---|---|---|
Availability POST /v1/orders |
99.9% | 30 days | 0.1% | 43 min |
Latency GET /v1/products < 300 ms |
99.5% | 30 days | 0.5% | 216 min |
| Saga confirmed < 60 s | 99.5% | 30 days | 0.5% | 216 min |
| Fresh outbox (< 100 pending) | 99.9% | 30 days | 0.1% | 43 min |
Why those numbers and not others: 99.9% on orders because every minute without selling costs measurable money (Black Friday, 01-05) and because the infrastructure from 05-02/05-04 (2 replicas, rolling without downtime) makes it achievable; 99.5% on latency and saga because 0.5% of slow pages or late confirmations is annoying but does not prevent buying, and because they depend on the PSP and on MongoDB at peak. The minutes math: 30 days × 24 h × 60 min × (1 − SLO); for 99.9%: 43,200 × 0.001 = 43.2 min. And a warning: minutes are an intuition; the real budget is measured in requests. A 100% outage for 43 minutes at 4 a.m. consumes far less budget (few requests) than 5% errors for 8 hours during business hours.
- Expressing the SLIs in PromQL with recording rules
Computing histogram_quantile or rate divisions over 30 days on every query is expensive. Recording rules precompute each SLI into a new series, with a conventional name sli:<name>:ratio_rate<window>, and the alerts and panels query that series:
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: slos-techcorp
namespace: techcorp
labels: { release: kube-prometheus-stack }
spec:
groups:
- name: sli-orders-availability
interval: 30s
rules:
# proportion of 5xx errors on POST /v1/orders, over several windows
- record: sli:orders_errors:ratio_rate5m
expr: |
sum(rate(http_requests_total{service="gateway", route="/v1/orders", method="POST", code=~"5.."}[5m]))
/ sum(rate(http_requests_total{service="gateway", route="/v1/orders", method="POST"}[5m]))
- record: sli:orders_errors:ratio_rate30m
expr: |
sum(rate(http_requests_total{service="gateway", route="/v1/orders", method="POST", code=~"5.."}[30m]))
/ sum(rate(http_requests_total{service="gateway", route="/v1/orders", method="POST"}[30m]))
- record: sli:orders_errors:ratio_rate1h
expr: |
sum(rate(http_requests_total{service="gateway", route="/v1/orders", method="POST", code=~"5.."}[1h]))
/ sum(rate(http_requests_total{service="gateway", route="/v1/orders", method="POST"}[1h]))
- record: sli:orders_errors:ratio_rate6h
expr: |
sum(rate(http_requests_total{service="gateway", route="/v1/orders", method="POST", code=~"5.."}[6h]))
/ sum(rate(http_requests_total{service="gateway", route="/v1/orders", method="POST"}[6h]))
- record: sli:orders_errors:ratio_rate30d
expr: |
sum(rate(http_requests_total{service="gateway", route="/v1/orders", method="POST", code=~"5.."}[30d]))
/ sum(rate(http_requests_total{service="gateway", route="/v1/orders", method="POST"}[30d]))
- name: sli-catalog-latency
rules:
- record: sli:catalog_slow:ratio_rate5m
expr: |
1 - (
sum(rate(http_request_duration_seconds_bucket{service="catalog-service", route="/v1/products", le="0.3"}[5m]))
/ sum(rate(http_request_duration_seconds_count{service="catalog-service", route="/v1/products"}[5m]))
)
- name: sli-saga
rules:
- record: sli:saga_late:ratio_rate1h
expr: |
1 - (sum(rate(saga_duration_seconds_bucket{le="60"}[1h])) / sum(rate(saga_duration_seconds_count[1h])))
- name: error-budget
rules:
- record: slo:orders_budget_remaining:ratio
expr: 1 - (sli:orders_errors:ratio_rate30d / 0.001) # 1 = intact, 0 = exhausted, negative = exceeded- Each
recordrule stores the result ofexpras a new series everyinterval. The alerts in the next section combine the 5 m, 30 m, 1 h and 6 h windows; the "remaining budget" panel uses the 30 d one. - The failure proportions (errors, slow, late) are recorded rather than the success ones: burn rates are computed over failure.
- The latency one uses
1 − (good/total):bucket{le="0.3"}counts those that took ≤ 300 ms. slo:orders_budget_remaining:ratiois the metric shown to the business: "we have 62% of this month's budget left".
The 30-day windows require enough retention in Prometheus (or Thanos/Mimir for the long term, a mention). The four SLIs are also documented as text in techcorp/platform/observability/slos.md: definition, owner, target and review date.
- SLO-based alerts: multi-window burn rate
The naive alert "error rate > 1% for 5 minutes" has two problems: it fires on a two-minute spike that barely spends any budget, and it does not fire on a sustained 0.5% over three days that does exhaust it. The SRE book's alternative is to alert on the speed at which the budget is consumed (burn rate): 1× means that at the current pace the budget runs out exactly at the end of the 30-day window; 14.4× means it would run out in 2 days (30/14.4). Two windows are combined (a long one to make sure it is real, a short one so the alert clears soon after it is resolved):
| Alert | Long window | Short window | Burn rate | Budget consumed to fire | Severity |
|---|---|---|---|---|---|
| Fast | 1 h | 5 m | 14.4× | 2% in 1 h | critical (page) |
| Slow | 6 h | 30 m | 6× | 5% in 6 h | warning (ticket) |
For the 99.9% SLO (budget 0.001), 14.4× equals an error rate of 1.44% and 6× equals 0.6%:
- name: alerts-slo-orders
rules:
- alert: OrdersErrorBudgetBurnFast
expr: |
sli:orders_errors:ratio_rate1h > (14.4 * 0.001)
and
sli:orders_errors:ratio_rate5m > (14.4 * 0.001)
for: 2m
labels:
severity: critical
team: orders
slo: orders-availability
annotations:
summary: "POST /v1/orders is burning the error budget 14.4x faster than planned"
description: "5xx rate {{ $value | humanizePercentage }} over the last hour (SLO 99.9%). See runbook."
runbook: "https://runbooks.techcorp.internal/orders/error-budget-burn"
dashboard: "https://grafana.techcorp.internal/d/red-service?var-service=orders-service"
- alert: OrdersErrorBudgetBurnSlow
expr: |
sli:orders_errors:ratio_rate6h > (6 * 0.001)
and
sli:orders_errors:ratio_rate30m > (6 * 0.001)
for: 15m
labels: { severity: warning, team: orders, slo: orders-availability }
annotations:
summary: "POST /v1/orders is burning the error budget 6x faster than planned"
runbook: "https://runbooks.techcorp.internal/orders/error-budget-burn"exprwithand: both windows must exceed the threshold at the same time. The 1 h one keeps a spike of seconds from waking anyone; the 5 m one makes the alert resolve minutes after the cause is fixed (without it, it would stay active until the whole hour cleared).for: 2m: the condition must hold for two minutes before firing; against the noise of a single scrape.labels:severitydecides the channel (section 7);teamdecides the recipient;slogroups.annotations: what the on-call person reads at three in the morning. Alwaysrunbookanddashboard; an alert without a runbook is not deployed (Platform rule, checked by a linter in CI).
The same two alerts are replicated for catalog latency (CatalogLatencyBurnFast, budget 0.005: thresholds 7.2% and 3% slow) and the saga (SagaLateBurnFast, over sli:saga_late). With four SLOs and two alerts each, that is eight SLO alerts in total: few, meaningful and all tied to something the customer notices.
- Symptom-based alerts, and why not to alert on causes
SLO alerts cover "the customer is suffering". There is a second group of symptom-based alerts that anticipate that they will suffer or that something is broken even though the customer does not notice yet; they must be few and have a clear threshold:
- name: alerts-symptoms
rules:
- alert: OutboxStuck
expr: outbox_pending{service="orders-service"} > 100
for: 5m
labels: { severity: critical, team: orders }
annotations: { summary: "The Orders outbox has {{ $value }} unpublished events piling up", runbook: "https://runbooks.techcorp.internal/orders/outbox-stuck" }
- alert: DlqHasMessages
expr: rabbitmq_queue_messages_ready{queue=~".+\\.dlq"} > 0
for: 10m
labels: { severity: warning, team: "{{ if eq $labels.queue \"payments.stock.dlq\" }}payments{{ else }}orders{{ end }}" }
annotations: { summary: "Queue {{ $labels.queue }} has {{ $value }} messages", runbook: "https://runbooks.techcorp.internal/common/dlq" }
- alert: PodInCrashLoop
expr: max_over_time(kube_pod_container_status_waiting_reason{namespace="techcorp", reason="CrashLoopBackOff"}[5m]) > 0
for: 5m
labels: { severity: warning, team: platform }
annotations: { summary: "{{ $labels.pod }} in CrashLoopBackOff", runbook: "https://runbooks.techcorp.internal/platform/crashloop" }
- alert: CircuitOpen
expr: circuit_breaker_state == 2
for: 5m
labels: { severity: warning, team: orders }
annotations: { summary: "Circuit open toward {{ $labels.dependency }} from {{ $labels.service }}", runbook: "https://runbooks.techcorp.internal/common/circuit-open" }(The templated team in DlqHasMessages is a simplification; in practice there is one rule per queue with its owning team from 02-01.) Others of the same kind: CertificateExpiring (probe_ssl_earliest_cert_expiry from Blackbox Exporter, < 14 days, a mention), ReadyProbeFailing for more than 10 minutes, RabbitMQNodeDown.
What is not paged on: causes. "CPU > 80%", "memory > 90%", "disk at 70%" may mean a problem or an HPA doing its job (06-04); if the customer is not suffering and no symptom appears, there is nothing to do at three in the morning. They are left as panels and, at most, as severity: info to a Slack channel that is read in the morning. The discipline is this: page → the customer is suffering or will within minutes; ticket → something is wrong and must be fixed this week; info → good to know. Every alert that wakes someone up and requires no action is an alert to delete or downgrade.
- Alertmanager: routes per team, grouping, silences and inhibition
Prometheus evaluates the rules; Alertmanager decides whom to notify and how. The Platform team's configuration (alertmanager-config secret, versioned with the chart values):
route:
receiver: slack-platform # default destination
group_by: [alertname, team] # one notification per alert and team, not per pod
group_wait: 30s # wait 30 s for related alerts to arrive before the first notification
group_interval: 5m
repeat_interval: 4h # reminder if still active
routes:
- matchers: [severity="critical"]
receiver: pagerduty-oncall # page whoever is on call
continue: true # and keep evaluating: it also reaches Slack
- matchers: [team="orders"]
receiver: slack-orders
- matchers: [team="payments"]
receiver: slack-payments-communications
- matchers: [team="shopping-experience"]
receiver: slack-shopping-experience
- matchers: [team="platform"]
receiver: slack-platform
receivers:
- name: pagerduty-oncall
pagerduty_configs:
- routing_key_file: /etc/alertmanager/secrets/pagerduty
severity: critical
- name: slack-orders
slack_configs:
- api_url_file: /etc/alertmanager/secrets/slack
channel: "#alerts-orders"
title: '{{ .CommonLabels.alertname }} ({{ .Status }})'
text: '{{ range .Alerts }}{{ .Annotations.summary }} — <{{ .Annotations.runbook }}|runbook>{{ "\n" }}{{ end }}'
# slack-payments-communications, slack-shopping-experience, slack-platform: analogous
inhibit_rules:
- source_matchers: [alertname="RabbitMQNodeDown"]
target_matchers: [alertname=~"DlqHasMessages|OutboxStuck"]
equal: [namespace] # if RabbitMQ is down, do not also notify about its consequences- Routing by
severityandteam:criticalgoes to PagerDuty (on-call) and to the team's Slack (continue: true);warningonly to the team's Slack. The four channels correspond to the four teams from 02-01. - Grouping (
group_by): if 12 Catalog pods enterCrashLoopBackOff, one notification arrives with 12 alerts, not 12 messages. - Silences: during planned maintenance (a RabbitMQ migration on a Sunday) a silence is created in the Alertmanager UI with
matchers(team="orders", 2 h) and a comment; alerts are evaluated but not sent. - Inhibition: a "root" alert (
RabbitMQNodeDown) suppresses the derived ones (OutboxStuck,DlqHasMessages) so that on-call receives one notice, not ten. - Slack and PagerDuty are fictional in the course; the pattern works just the same with Opsgenie, Teams or email.
- On-call and runbooks
TechCorp, with ~25 engineers, sets up one weekly on-call rotation among the developers of the four teams (not just Platform: whoever builds, operates), with a primary and a secondary person, an agreed compensation schedule and one rule: on-call only receives critical alerts with a runbook. Everything else waits for business hours.
A runbook is the page opened from the alert that says what to look at and what to do, written for someone who does not know the service and is half asleep. Template and example for OrdersErrorBudgetBurnFast:
# Runbook: OrdersErrorBudgetBurnFast
Severity: critical · Team: Orders · Last reviewed: 2026-08-01 · Owner: Luis
## What it means
POST /v1/orders is returning 5xx to more than 1.44% of requests over the last hour
and the last 5 min. Customers cannot buy. Every minute counts against the 99.9% SLO.
## Check within 5 minutes
1. Grafana "RED per service" (service=gateway, route=/v1/orders): which codes? 503 → dependency; 502/504 → Orders not responding; 500 → bug.
2. Grafana "RED per service" (service=orders-service): high latency? pods alive? kubectl -n techcorp get pods -l app=orders-service
3. Loki: {app="orders-service", level="error"} | json | line_format "{{.err.code}} {{.message}}"
- DEPENDENCY_UNAVAILABLE + dependency=catalog → go to "Catalog down" (below)
- pg errors → go to "Database"
4. Has there been a deployment in the last hour? argocd app history orders-service
Yes → roll back first, investigate later: argocd app rollback orders-service <previous-id>
Canary (05-04): kubectl -n techcorp annotate ingress orders-canary nginx.ingress.kubernetes.io/canary-weight=0
5. Jaeger: search service=orders-service, error=true, last 15 min: which span fails?
## Remedies by cause
- Catalog down: check catalog pods; the breaker (06-03) already protects; if it is MongoDB → escalate to Platform.
- Database: kubectl -n techcorp get pods -l app=postgres; connections: SELECT count(*) FROM pg_stat_activity; if at the maximum, restart the orders pod with the most idle connections.
- Pods in CrashLoop: kubectl -n techcorp logs -l app=orders-service --previous | head; usually config (04-03) → review the last ConfigMap change.
- Messages in DLQ after recovery: node scripts/reprocessDlq.js --queue orders.saga --max 200 (06-03) from an ephemeral pod.
## Escalate to
Luis (Orders lead) after 15 min without a cause; Platform if it is infrastructure; Marta if SEV1 (> 30 min or Black Friday).
## Afterwards
Record in the #incidents channel; if it lasted > 15 min or affected customers, open a postmortem (template).Every alert in the table in section 12 has its own in techcorp/platform/runbooks/, versioned in Git and with a last-reviewed date: a runbook with commands that no longer work is worse than none. The golden rule is that whoever resolves an incident without a runbook writes it when done.
- Incident management: roles, severities and communication
An incident is any situation that degrades the service and requires coordination. When a page fires, the on-call person does three things: acknowledges the alert (so it does not keep escalating), opens a channel (#inc-2031-payments-stock in Slack) and assesses the severity:
| Severity | Criterion | TechCorp example | Response |
|---|---|---|---|
| SEV1 | Customers cannot buy, or there is data loss/damage | Gateway down; POST /v1/orders with > 50% 5xx; duplicate charges |
Immediate page, commander assigned, communication to the business every 30 min, all teams available |
| SEV2 | Significant degradation or part of the flow broken, with a workaround or partial impact | Stuck saga (orders in STOCK_RESERVED without confirmation); slow catalog with p95 > 1 s; notifications not going out |
Page, resolved in extended hours, communication at the start and at closure |
| SEV3 | Minor or internal impact; no visible effect for the customer | DLQ with 3 messages; one pod in CrashLoopBackOff with healthy replicas; error budget at 20% |
Ticket, resolved during business hours |
The roles in a SEV1/SEV2, so that not everyone types at once:
- Incident commander: coordinates, decides and assigns; does not debug personally. Usually the on-call person until someone with more context explicitly takes over ("I'm taking command").
- Communicator: keeps the business and support informed (a message every 30 min in
#platform-statuswith: what is happening, impact, what is being done, next update) so that nobody interrupts those resolving it. - Operators/investigators: one or several; they follow the runbook, execute changes and note every action with its time in the channel.
Principles: stabilize before understanding (roll back the deployment, scale replicas, disable the canary, reprocess later: the root cause is sought once the customer is no longer suffering); a real-time timeline in the channel (the postmortem will reconstruct it); and communication with the business in their language ("orders cannot be created since 10:14; we estimate recovery in 20 min; orders already created are not affected"), never in ours ("payments.stock is stuck because of a poison message").
- Blameless postmortem: INC-2031
Every SEV1 and SEV2 ends with a postmortem in the following week. "Blameless" means it is assumed that people acted reasonably with the information they had, and that the question is what in the system (code, tools, processes, alerts) allowed it to happen and to last as long as it did. If the postmortem looks for someone to scold, next time the information gets hidden. Template and summarized example:
# Postmortem INC-2031 — payments.stock stuck for 40 minutes by a poison message
Date: 2026-07-22 · Severity: SEV2 · Duration: 40 min (10:14–10:54) · Commander: on-call (Elena, Payments)
Authors: Payments & Communications team · Status: actions in progress
## Summary
A stock.reserved event with `lines: null` (bug in Inventory after deployment 2.3.0) made the
payments.stock consumer throw a TypeError. With prefetch(1) in Payments and immediate retry with requeue,
the message went back to the head of the queue in a loop and blocked the other charges. 61 orders were left in
STOCK_RESERVED; the watchdog cancelled 38 with PAYMENT_TIMEOUT before resolution.
## Impact
61 customers without confirmation for up to 40 min; 38 orders cancelled and notified as "payment problem"
(23 bought again). Saga error budget consumed: 31% of the month.
## Timeline (UTC+2)
10:03 Deployment inventory-service 2.3.0 (canary 10% → 100% at 10:10).
10:14 First TypeError in payments.stock (Loki). No alert: neither DlqHasMessages nor SagaLateBurn existed.
10:31 Support raises it in Slack: customers without confirmation. On-call opens #inc-2031.
10:38 Saga dashboard: 40 orders in STOCK_RESERVED; payments.stock with 58 messages ready and none consumed.
10:44 The poison message is identified in Loki by eventId; it is purged from the queue by hand.
10:46 Queue unblocked; pending charges are processed in 2 min.
10:50 Inventory rolled back to 2.2.4 (argocd rollback).
10:54 Saga back to normal. Incident closed.
## Causes (5 whys, summarized)
1. Inventory published an invalid event → no outbound validation against the contract (03-06); the Pact test
did not cover null lines.
2. Payments retried in a loop → nack with immediate requeue and prefetch(1) (06-03 was not yet applied).
3. Nobody saw it for 17 min → no symptom alert on queues nor SLO alert on the saga; the panel existed, nobody watches it.
4. It took 13 more minutes to locate → no runbook for "stuck queue".
## What worked
The PAYMENT_TIMEOUT watchdog limited the damage (reservations released, customers notified). The logs by eventId made it
possible to locate the message in minutes once we knew where to look.
## Corrective actions (tasks with owner and date)
- [Payments] Retry queue with TTL + DLQ on the first try for permanent errors; prefetch 3. → done in 06-03
- [Orders] DlqHasMessages and SagaLateBurnFast/Slow alerts. → this lesson
- [Inventory] Validate outbound events against the contract schema before writing to the outbox; Pact test.
- [Platform] "Stuck queue / DLQ" runbook and reprocessDlq.js script. → done in 06-03
- [Everyone] Add "which alert would have caught it?" as a standing postmortem question.The actions become tasks in the backlog with an owner and a date, and are reviewed at the next operations meeting; a postmortem whose actions are not carried out is a useless document. And it is shared with the whole company: incidents are the most expensive source of learning there is; wasting it is a double loss.
- SLO review and error budget policy
SLOs are not forever. Every quarter, each team reviews with product: does the SLI measure what the customer notices? Is the target too ambitious (always red, nobody believes it) or too loose (always 100%, it says nothing)? Have expectations changed (a new marketplace demands more)? Targets are adjusted, useless SLIs retired and missing ones added.
And the error budget is used with a written policy, agreed between Marta and the teams:
| Budget remaining in the month | Policy |
|---|---|
| > 50% | Normal: deployments, experiments, game days (06-03) |
| 20-50% | Caution: risky deployments are reviewed with another person; longer canary (05-04) |
| < 20% | Only reliability changes and fixes; features are frozen until the budget recovers (the 30-day window moves forward) |
| Exhausted / negative | Total freeze except fixes; the team devotes the sprint to reliability; mandatory postmortem for every contributor |
It is the balance that was missing in the velocity debate of 05-03: the DORA metrics push toward deploying more often; the error budget says up to when. A team with budget to spare should dare more (more deployments, more experiments), not less: not spending the budget is also a waste.
- Summary table of TechCorp's alerts
| Alert | Condition | Severity | Team | Runbook |
|---|---|---|---|---|
OrdersErrorBudgetBurnFast |
5xx on POST /v1/orders > 1.44% over 1 h and 5 m |
critical | Orders | orders/error-budget-burn |
OrdersErrorBudgetBurnSlow |
> 0.6% over 6 h and 30 m | warning | Orders | orders/error-budget-burn |
CatalogLatencyBurnFast / Slow |
GET /v1/products > 300 ms in > 7.2% (1 h/5 m) / > 3% (6 h/30 m) |
critical / warning | Shopping Experience | catalog/latency |
SagaLateBurnFast / Slow |
Sagas > 60 s in > 7.2% / > 3% | critical / warning | Orders | orders/saga-stuck |
OutboxStuck |
outbox_pending > 100 for 5 m |
critical | Orders | orders/outbox-stuck |
DlqHasMessages |
Any *.dlq with messages for 10 m |
warning | Queue owner | common/dlq |
CircuitOpen |
circuit_breaker_state == 2 for 5 m |
warning | Service owner | common/circuit-open |
PodInCrashLoop |
CrashLoopBackOff in techcorp for 5 m |
warning | Platform | platform/crashloop |
RabbitMQNodeDown |
RabbitMQ cluster node unavailable | critical | Platform | platform/rabbitmq |
CertificateExpiring |
TLS certificate < 14 days | warning | Platform | platform/certificates |
ErrorBudgetLow |
slo:*_budget_remaining:ratio < 0.2 |
info | Each team | common/budget-policy |
Eleven alerts (plus the per-queue variants). If in three months one of them has never required action, it is downgraded or removed; if an incident was not detected by any, the one that would have caught it is added.
Common Mistakes and Tips
- A hundred alerts. Each one seems reasonable; together they cause fatigue and on-call ignores them all. Start with the SLO ones and four symptoms; add only from postmortems.
- Alerting on a fixed error threshold ("> 1% for 5 m"). Noise on spikes, silence on slow degradations. Multi-window burn rate.
- A 99.99% SLO "because it sounds good". That is 4 minutes a month: incompatible with dependency rolling updates, with the PSP and with the company's budget. Choose what the business needs and the architecture allows.
- Measuring the SLI where it is convenient and not where the customer is. Orders at 100% with the gateway down is a met SLO and a closed store.
- Alerts without a runbook. Waking someone up to start from scratch. Runbook or it is not deployed.
- Postmortems with names and adjectives ("X deployed without testing"). Next time nobody will tell what happened. System, not people.
- Postmortem actions without owner or date. Forgotten within a week. Tasks in the backlog, reviewed.
- On-call only in Platform. Developers do not see the consequences of their code. Whoever builds, operates (with training and compensation).
- Tip: build a single "SLO status" dashboard with the four proportions and the remaining budget, and put it on the team's screen; it is the conversation with product that was missing.
- Tip: test the alerts:
amtool alert add OrdersErrorBudgetBurnFast severity=critical team=orderssends a test alert and checks the routing without waiting for a real incident.
Exercises
Exercise 1: SLO and budget for Notifications
Define an SLI and an SLO for notifications-service ("the customer receives the confirmation email"), state which metric it would come from (you may propose a new one consistent with 06-01), compute the monthly budget and justify a severity for its alert.
Exercise 2: the saga alert
Write the complete SagaLateBurnFast rule (expression with the necessary recording rules, for, labels, annotations) for the 99.5% SLO, and explain why its threshold is 7.2% and not 1.44%.
Exercise 3: classify and act
It is 02:10. OutboxStuck fires (outbox_pending = 340) and, at the same time, DlqHasMessages on inventory.orders.dlq with 2 messages. POST /v1/orders responds 202 as usual. Classify the severity, decide which alert you handle first and describe the first five minutes in the style of the runbook.
Solutions
Exercise 1
SLI: proportion of confirmation emails handed to the provider in under 5 minutes from order.confirmed, over the total of order.confirmed received. Metric: histogram notification_latency_seconds{type="confirmation"} observed in Notifications from the event's occurred_at until the provider's 2xx response, with buckets [1, 5, 30, 60, 300, 900]; SLI = rate(..._bucket{le="300"}) / rate(..._count). SLO: 99% over 30 days (a late email is annoying, it does not prevent buying; the external provider does not guarantee more). Budget: 1% ≈ 432 minutes of "total outage" per month, or about 900 late confirmations out of 90,000. Alert: NotificationsLateBurnFast with severity: warning (not critical): it wakes nobody up; emails are retried from the queue (06-03) and arrive when the provider comes back. If the delay exceeded hours it would become SEV3/SEV2 for the impact on support, but not a page.
Exercise 2
Recording rules needed: sli:saga_late:ratio_rate1h (already defined) and sli:saga_late:ratio_rate5m (same expression with [5m]).
- alert: SagaLateBurnFast
expr: |
sli:saga_late:ratio_rate1h > (14.4 * 0.005)
and
sli:saga_late:ratio_rate5m > (14.4 * 0.005)
for: 2m
labels: { severity: critical, team: orders, slo: saga-on-time }
annotations:
summary: "Sagas are taking longer than 60 s for {{ $value | humanizePercentage }} of orders"
description: "The error budget of the 'saga < 60 s' SLO (99.5%) is being burned 14.4x faster than planned. Usually Payments, Inventory or RabbitMQ."
runbook: "https://runbooks.techcorp.internal/orders/saga-stuck"
dashboard: "https://grafana.techcorp.internal/d/saga-orders"The threshold is burn rate × budget: 14.4 × 0.005 = 0.072 (7.2%). With a 99.5% SLO the budget is five times larger than with 99.9%, and therefore the failure rate that exhausts it "in two days" is also five times larger. The burn rate is the same (14.4×); the absolute percentage depends on the SLO.
Exercise 3
Severity: POST /v1/orders keeps accepting orders, but none of them progresses (events do not leave the outbox): the saga is stopped for everyone buying right now, and in 10 minutes the watchdog will not act (the orders are in PENDING, not in STOCK_RESERVED), so unconfirmed orders will pile up. It is a SEV2 (broken flow with growing impact; not SEV1 because there is no data loss: the outbox keeps everything). OutboxStuck is handled first: it is critical, it is the most "upstream" cause, and Inventory's DLQ with 2 messages is warning and can wait (it might also be a consequence: if RabbitMQ is misbehaving, both alerts fit). Five minutes: (1) acknowledge in PagerDuty, open #inc-XXXX-outbox; (2) Grafana "Orders saga": outbox_pending climbing in a straight line since 01:55, orders_created_total normal, rabbitmq_queue_messages_ready{queue="inventory.orders"} = 0 → the relay is not publishing; (3) kubectl -n techcorp get pods -l app=orders-service → 2 pods Running; Loki {app="orders-service"} | json | message=~"relay.*|rabbit.*" → "channel closed by the broker" at 01:55 and no "batch published" since then: the relay lost its channel and did not reconnect (a reconnection bug); (4) immediate remedy: kubectl -n techcorp rollout restart deployment/orders-service (the new pods open a channel, the relay drains the outbox in a minute; the rolling update from 05-04 does not interrupt POST); check that outbox_pending drops; (5) note the timeline, look at the 2 messages in inventory.orders.dlq (probably related: events published halfway before the cut; they are reprocessed with reprocessDlq.js once the relay is running), message to the communicator/#platform-status and, in the morning, a postmortem with the action "the relay must reconnect with backoff and /health/ready must fail if the channel is closed" (06-03).
Conclusion
With this lesson TechCorp stops "looking at dashboards" and starts operating against objectives. Four SLIs say what the customer notices (availability of POST /v1/orders, latency of GET /v1/products under 300 ms, sagas confirmed in under 60 s, outbox freshness), with 99.9%/99.5% SLOs and their error budget in minutes and in requests; the sli:*:ratio_rate<window> recording rules precompute them; the multi-window burn rate alerts (OrdersErrorBudgetBurnFast at 14.4× over 1 h/5 m, …Slow at 6× over 6 h/30 m) and a handful of symptom alerts (OutboxStuck, DlqHasMessages, CircuitOpen, PodInCrashLoop) reach the owning team through Alertmanager and, only if critical, on-call; each with a runbook; incidents are managed with SEV1-SEV3 severities, a commander and a communicator, and end in a blameless postmortem whose actions are tasks; and the error budget policy decides when to deploy and when to stop. This closes the monitoring and maintenance module: TechCorp's services emit structured logs and RED and business metrics (06-01), traces that join the HTTP request with the saga (06-02), survive failures with timeouts, retries, circuit breakers, retry queues and the saga watchdog (06-03), scale on their own and with load tests (06-04) and are operated against SLOs with alerts, on-call and postmortems (06-05). The system is now observable, resilient, scalable and operable; what it still is not, apart from the mentions of JWT and data redaction, is secure. Module 7 starts there: authentication and authorization with JWT/OAuth2 and Keycloak (07-01), security in communication between services (07-02), security practices in code and data (07-03) and container and Kubernetes security (07-04).
Microservices Course
Module 1: Introduction to Microservices
- Basic Concepts of Microservices
- Advantages and Disadvantages of Microservices
- Comparison with the Monolithic Architecture
- When to Adopt Microservices: Decision Criteria
- The Course Case Study: TechCorp's Online Store
Module 2: Microservice Design
- Microservice Design Principles
- Decomposing Monolithic Applications
- Defining Bounded Contexts
- Data Management: One Database per Service
- Distributed Consistency: Sagas, CQRS and Event Sourcing
Module 3: Communication between Microservices
- RESTful APIs
- Asynchronous Messaging
- Communication Protocols: gRPC, GraphQL
- API Gateway and Backend for Frontend
- Service Discovery and Load Balancing
- API Contracts and Versioning
Module 4: Implementing Microservices
- Choosing Technologies and Tools
- Building a Simple Microservice
- Configuration Management
- Hands-On Integration: Consuming APIs and Publishing Events
- Testing Microservices: Unit, Integration and Contract Tests
Module 5: Deployment and Orchestration
- Containers and Docker
- Orchestration with Kubernetes
- CI/CD for Microservices
- Deployment Strategies: Rolling, Blue-Green and Canary
- Service Mesh: Istio and Linkerd
Module 6: Monitoring and Maintenance
- Monitoring and Logging
- Distributed Tracing with OpenTelemetry
- Error Handling and Recovery
- Scalability and Performance
- SLOs, Alerts and Incident Management
Module 7: Security in Microservices
- Authentication and Authorization
- Communication Security
- Security Practices
- Container and Kubernetes Security
