Operations
Software in production is a continuous system, not a deliverable. The CI/CD pipeline that landed yesterday's change is the first ten minutes of a service's life; the next ten years are operations. This page assumes the overview pass from Act IXb — what an SLO is, why postmortems are blameless, what a flame graph looks like — and goes one layer down into the engineering. The multi-burn-rate alert math, the coordinated-omission trap, the M/M/1 response curve, the RPO/RTO arithmetic behind a regional failover. These are the things on-call engineers reach for at 03:14 when the dashboard turns red.
Capacity planning
A service that runs at 30% average CPU and falls over every Friday at 18:00 has a capacity problem the dashboard never showed. The average smooths over the moment the system actually does the work. Capacity planning is the discipline of choosing how much hardware (or how much auto-scaling budget) the service needs so that the worst expected hour still meets the SLO, with a margin for the spike that the forecast missed.
Start with the load profile. Three numbers matter: average load, peak load, and the peak-to-average ratio. A typical web service running on user time has a diurnal peak around 3–5× the daily average. A consumer service with a single-time-zone audience can run 8–10×. An internal back-office service might run at 1.5×. The ratio is what determines how much headroom the system needs over its mean.
The next number is growth. Compute the monthly growth rate from the last six months of traffic; project it forward. A service growing 8% month-over-month doubles in nine months. If today's peak utilisation is 60% and the team plans a single capacity review per year, the service hits 120% of capacity in the same year — the plan ships an outage. Growth tracking is what turns capacity from a one-time provision into a quarterly check.
The headroom math is the simplest piece. If the SLO requires the service to absorb the diurnal peak at acceptable latency, and the M/M/1 response curve says latency stays bounded only under around 70% utilisation, then the steady-state peak target is 70%, the growth-adjusted six-month peak target is 50%, and the average runs wherever it lands. Provision to the target peak, not to the average. The cloud bill is set by peak; the user experience is set by peak; the average is for the spreadsheet.
Queue theory gives the hard constraint underneath. A service with arrival rate λ and service rate μ accumulates a queue whenever λ ≥ μ. There is no graceful degradation past that line — work piles up, latency grows without bound until something breaks. The whole point of headroom is to keep λ comfortably below μ at the worst expected moment. The peak-to-average ratio is the multiplier that turns "comfortably below μ on average" into "comfortably below μ at 18:07 on Black Friday."
The trade-off is real money. Under-provisioning costs the SLO. Over-provisioning costs the cloud bill. The sweet spot is a function of how spiky the traffic is, how quickly you can scale out (cold-start time), and how forgiving the SLO is to tail-latency events. A 99.9% SLO over 30 days allows about 43 minutes of badness; a 99.99% SLO allows about 4 minutes — and 4 minutes is roughly the time it takes most autoscalers to add capacity in response to a load shift. The headroom you keep is the latency budget you stop spending on the autoscaler's reaction time.
Capacity planning is the first move in operations because every later mechanism — auto-scaling, queue draining, circuit breaking — assumes the system has somewhere to scale into. None of them save you from an undersized fleet at peak.
Performance profiling
A service is slow. The dashboard shows p99 latency at 800 ms when the SLO target is 200 ms. Adding hardware costs money and may not help; the bottleneck might be a lock, a GC pause, or a hot key. Profiling is how you find out — by sampling the running process often enough to build a statistical picture of where time is actually spent, and then reading that picture honestly.
Two axes structure the choice of tool. The first is CPU time vs wall time. CPU time counts cycles the thread was running on a core; wall time counts cycles plus everything the thread waited on — locks, I/O, the scheduler, the network. A CPU profile shows a tight inner loop chewing 40% of cycles; a wall-time profile shows that same thread spending 60% of its day blocked on a single database call. Both are useful, both lie if read as the other. The second axis is sampling vs instrumentation. A sampling profiler interrupts the process N times a second (commonly 99 Hz to avoid harmonic aliasing) and records the stack; overhead is bounded and low. Instrumentation profilers add a counter around every function entry/exit; the data is exact but the overhead can change what you measure.
Reading a profile is the skill people skip. The widest leaf is not the bug; it's the symptom. A 30%-wide reflect.Value.Field bar in a JSON encoder means reflection is expensive — that is not news. The question is why the code is calling reflective JSON encoding 50,000 times a second when 95% of the responses serialise the same five fields. The optimisation lives at the caller, not the leaf. Train yourself to walk up the stack from the hot leaf to the first frame your team owns, then ask whether that frame is doing the right amount of work.
Allocation profiling is the sibling that catches memory bugs masquerading as CPU bugs. A function that allocates ten objects per request burns CPU on the allocator and on the garbage collector long after the function returns. go tool pprof -alloc_objects or jcmd <pid> GC.heap_info show the allocation profile; a hot allocator usually means a hot GC, which means GC pauses bleeding into tail latency. The fix is rarely "tune the GC" and usually "stop allocating in the hot path" — pooled buffers, pre-sized slices, fewer string concatenations.
Lock contention and off-CPU analysis catch the inverse problem: time spent waiting that a CPU profile cannot see. A process pinned at 20% CPU with terrible p99 latency is almost certainly blocking on something — a mutex held by another goroutine, a connection-pool semaphore, a synchronous fsync. Off-CPU analysis, Brendan Gregg's framing, samples the off-CPU stack — what was the thread waiting for when it wasn't running? eBPF makes this affordable in production: offcputime-bpfcc traces blocking events without instrumentation, and tools like Pyroscope and Parca run continuous off-CPU and on-CPU profilers at around 1% overhead, sampled per pod, for weeks at a time.
The toolchain by language is worth memorising. pprof (Go, also via gperftools for C++) emits a sampled CPU/heap profile that go tool pprof renders as a flame graph or a callgraph. perf (Linux, kernel-level) traces hardware counters and stacks with high fidelity; pair with FlameGraph scripts. async-profiler (JVM) samples with safepoint-bias-free stacks and emits flame graphs directly. py-spy and rbspy sample CPython and CRuby without instrumenting the target. eBPF-based continuous profilers (Pyroscope, Parca, Polar Signals) compile probes into the kernel and stream sampled stacks to a backend with low constant overhead — the current default for long-running services.
The trade-off is overhead and accuracy. A higher sample rate gives a sharper picture and costs more CPU. Instrumented profilers can perturb the program enough to chase phantom hotspots. The honest sample rate is whatever is high enough to find the bottleneck and low enough not to be the bottleneck — usually 99 Hz for production CPU profiling, with bursts to 1 kHz when debugging a known regression. Capacity told you the fleet was undersized; profiling tells you whether a smaller fleet would suffice if the inner loop weren't burning cycles on reflection.
Load testing
Capacity planning estimates load; load testing proves the estimate. A service should never see its peak production load for the first time on the day it ships. Load testing is the practice of synthetically driving traffic at the system to confirm it handles the planned profile, expose the bottleneck that wasn't on the diagram, and pre-burn the runbook.
Four shapes cover most needs. A steady-state test holds a target RPS for an extended window — the baseline measurement. A soak test holds steady-state for hours or days to find leaks: memory growth, connection-pool exhaustion, log volume creep, descriptor leaks. A stress test ramps load past the target until the system fails, to characterise the breakpoint. A spike test jumps from baseline to 5× or 10× in one step, modelling a flash event, to see whether the autoscaler reacts before the SLO does.
The hidden variable is the generator model. A closed-loop generator maintains a fixed number of virtual users, each of which sends a request, waits for the response, then sends the next. If the server takes longer to respond, the generator naturally sends fewer requests per second — the open-system load (the rate from a real client population that doesn't know or care about server health) is not what the test produces. The system under test appears to behave better than it actually would. This is coordinated omission, named by Gil Tene: the latency samples the generator collected are biased by the very latency it was supposed to be measuring.
An open-loop generator issues requests at a target rate regardless of server response time. If the target is 500 RPS and the server slows to 1 s per request, the generator queues the backlog and reports the queueing wait as part of the client-observed latency — which is what a real user would experience. wrk2 (Gil Tene's fork of wrk) pioneered the corrected model; k6, vegeta, and current-generation load tools either default to open-loop or expose it explicitly. JMeter and most ad-hoc scripts default to closed-loop; their latency histograms are usually optimistic by an order of magnitude on the tail.
Worked example: reading a load-test result
A load test reports the following at a target of 500 RPS for 10 minutes against a single instance:
- Throughput: 500 RPS achieved, no errors.
- Latency: mean 60 ms, p50 50 ms, p99 200 ms, p99.9 1.4 s, max 3.8 s.
What does this tell you?
Throughput confirms the generator hit its target. No errors means the service accepted every request. So far so good.
Little's law (L = λW) gives the concurrency level. With λ = 500 RPS and W = mean latency = 0.060 s, average in-flight requests L = 500 × 0.060 = 30. The server held about 30 requests in flight at any instant. If your worker pool is 32, you ran the test right at the edge of the pool — anything more and the queue would build.
The mean is uninformative. Mean = 60 ms; p50 = 50 ms — those agree, fine. But p99 = 200 ms means 1 in 100 requests waited 200 ms, and p99.9 = 1.4 s means 1 in 1000 waited 1.4 seconds. The mean buries that. At 500 RPS, p99.9 fires about 30 times a minute — a user-visible event roughly every two seconds.
The tail tells the story. A factor of 7× between p99 and p99.9 is the signature of a discrete event: a GC pause, a TCP retransmit timeout, a connection-pool wait, a cache miss that fell through to a cold disk read. A 3.8 s max in a 10-minute test with 300,000 requests is one outlier, probably a single GC pause or a single network jitter event — note it but don't chase it. A 1.4 s p99.9 is 300 occurrences in the test, a real bottleneck. The investigation is at p99.9, not at the max.
Suspect coordinated omission. If the generator was closed-loop, the real tail is worse than reported. Re-run with wrk2 or k6 (open-loop) at the same target rate; if p99.9 jumps from 1.4 s to 5 s, you had been measuring the generator's self-throttle.
Next step. Flame-graph the worker during the test. If reflection or allocation tops the on-CPU profile, look there. If the on-CPU profile is unremarkable, run the off-CPU profile — the tail is almost certainly time spent blocked, not time spent computing. Common culprits: a hot-key contention on a shared lock, a head-of-line block on a single-threaded downstream, a GC pause whose frequency matches the tail event rate.
The conclusion is not "the system passed" or "the system failed." It's "the system runs 500 RPS but with a p99.9 the SLO won't tolerate at 5× scale — fix the tail before raising the cap."
The trade-off in load testing is realism vs cost. The most honest test runs against a production-shaped fleet with production-shaped data, from a generator outside the same datacenter (so the network behaves), at a duration long enough to expose leaks. That bill is real. Shorter, smaller tests catch most regressions; the long realistic test is what you run before a launch you cannot afford to roll back. Treat load tests as an investment that pays off in the incidents you don't have, not as a checkbox before the release.
Auto-scaling and queue theory
Static capacity is wrong twice a day: too much at 04:00, too little at 18:00. Auto-scaling is the practice of letting a controller add or remove capacity as load changes, so the fleet tracks the diurnal curve and the unexpected spike. The mechanism is simple — measure something, compare to a threshold, request more or fewer instances. The hard part is choosing the metric, the threshold, and what to do during the seconds-to-minutes it takes new capacity to start serving traffic.
Little's law (L = λ × W) is the universal constraint. For any stable queueing system, the average number of items in the system (L) equals the average arrival rate (λ) times the average time each item spends in the system (W). It holds across architectures: web servers, message queues, thread pools, garbage collectors. If you know any two, the third is determined. A service handling 500 RPS at 60 ms mean latency holds 30 requests in flight; if those 30 requests need 30 worker threads and you have 28, you have a queue.
The M/M/1 response curve says response time as a function of utilisation ρ = λ/μ is W = 1 / (μ − λ), which approaches infinity as ρ → 1. The practical reading: latency is roughly flat from 0 to about 50% utilisation, starts to climb noticeably at 60%, doubles by 70%, and goes hyperbolic past 80%. Real systems are not M/M/1 (the M/M/c case for multiple servers is gentler, but the qualitative shape is the same), and you should treat 70% utilisation as the target ceiling under which capacity must keep the service.
Reactive scaling triggers on a measured signal. The classic targets are CPU utilisation, requests-per-second per instance, or a queue depth. AWS calls the gentle version target tracking — pick a target metric (CPU = 60%), let the controller add or remove instances to keep the metric at target. The aggressive version is step scaling — at 70% CPU add 1 instance, at 80% add 3, at 90% add 6. Step scaling reacts faster to a sudden ramp; target tracking is calmer in steady state. Most fleets use both — target tracking as the baseline, step scaling for the spike.
Predictive scaling forecasts demand from history and warms capacity before it's needed. A service that always peaks at 18:00 can pre-scale at 17:45. The forecast can be naive (yesterday at this time) or model-based (Prophet, ARIMA, a recurrent net). Predictive scaling is essential when cold-start latency is a meaningful fraction of the SLO window — a service that takes 90 seconds to come up cannot react to a spike in time; it must already be warm.
The math of "how many instances per N RPS" is concrete. Suppose a single instance handles 100 RPS at 60% CPU. The headroom rule says target 60% peak, so each instance contributes 100 RPS of capacity. At 1000 RPS expected peak you need 10 instances; growth-adjusted six-month peak target says provision 14. If the autoscaler can add 1 instance every 30 seconds and the worst spike doubles load in 60 seconds, the autoscaler will catch up only after 2 minutes of degraded service — which is 120 seconds of error budget. A warm pool of pre-baked, stopped instances cuts cold-start from 90 s to 10 s and is often cheap relative to the alternative of running the spare capacity hot.
Horizontal vs vertical is the choice axis above. Horizontal (more instances) is the default for stateless services and the only option once you exhaust the biggest available machine. Vertical (bigger instance) is occasionally cheaper for stateful single-process bottlenecks (a cache, a database leader) where horizontal would require sharding. Vertical scaling has a hard ceiling and an outage during the resize.
The trade-off is the autoscaler's own failure modes. A bad metric (CPU on an I/O-bound service) scales the wrong axis. A short cooldown produces flapping — scale up, briefly cool, scale down, scale up again. A long cooldown leaves the fleet over-provisioned for hours after a spike. The honest setup: pick the metric closest to user-visible load (RPS per instance is usually better than CPU), set a target around 60–70%, give the cooldown enough time that the new instance is healthy before the controller acts again, and run a warm pool sized for the worst expected ramp. Auto-scaling is not a substitute for capacity planning — it's the controller that executes the plan.
Distributed tracing in depth
A user reports that one checkout took 8 seconds. The metrics dashboard shows p99 at 200 ms — that user's request is one in five million. Logs don't help: there are billions, and the user-visible 8-second wait spans seven services. Distributed tracing is the telemetry that survives this. A request gets a trace_id at the edge; every service it touches emits a span with the trace id, a parent span id, a service and operation name, start and end timestamps, and a small bag of attributes. Reassembled, the spans form a tree showing where the 8 seconds went.
The structural primitives are a span (one unit of work in one service), a parent-child link (which span caused which), a trace (the whole tree), and baggage (small key-value pairs carried across the request, such as a tenant id used downstream for log enrichment). The W3C Trace Context specification standardises the wire format: a traceparent header carrying 00-<trace-id>-<parent-span-id>-<flags> and an optional tracestate header for vendor-specific metadata. Every current client library — OpenTelemetry, language-specific OTel SDKs, service-mesh sidecars — propagates these headers automatically across HTTP, gRPC, and most messaging protocols.
The hard problem is what to sample. A million requests per second produces a million traces; storing all of them is the same cost as duplicating the application logs at higher fidelity. Head sampling decides at the edge: flip a coin (typically 1% to 0.01%) and either trace the request or don't, propagating the decision through the headers. Cheap, predictable, and structurally blind to interesting tails — the one slow request in a million has a 999,999/1,000,000 chance of being thrown away at the head.
Tail sampling decides at the end: collect every span temporarily (in a sidecar collector with bounded buffer), and once the trace completes, keep it only if it matches a rule — errored, exceeded a latency threshold, touched a critical service, was tagged for inspection. Tail sampling lets you keep every slow trace and 0.1% of fast ones, which is what investigators actually want. The cost is memory and complexity: the collector must hold spans for at least the trace's duration before deciding, and out-of-order spans must be reassembled across many collector nodes (typically by trace-id sharding).
Exemplars bridge metrics and traces. A histogram bucket — say, "requests in the 1 s–2 s latency bucket" — can carry a small set of trace ids of the requests that fell into that bucket. The Prometheus exposition format and OpenMetrics support this; click a hot bar on a latency histogram, jump straight to a real trace that landed in that bar. Exemplars turn a metric anomaly into a concrete request to investigate, without needing to keep every trace.
The trade-off is fidelity vs cost vs explainability. Head sampling at 1% costs less and explains tail behaviour worse; tail sampling at 100% with retention rules costs more and explains tail behaviour exactly. The sweet spot for most teams is tail sampling at the collector with errors and slow traces preserved, head sampling for the long fast-success tail, and exemplars wired into the metric pipeline so a dashboard graph clicks through to a real example. OpenTelemetry (CNCF) is the cross-vendor SDK and protocol — instrument once, switch backends without re-instrumenting. Use it; the alternative is rewriting every service when the vendor changes.
SLOs in production
The Act IXb overview covered what an SLO is: an SLI plus a target plus a window, and the error budget that follows from the gap to 100%. Production SLO engineering is the second-order question: given the math, how do you alert on burn so the on-call gets paged when a slow leak or a fast disaster is consuming the budget, but does not get paged on every transient blip?
The naive approach pages on the current rate. "The five-minute error rate is over 1 in 1000, page." This produces false alarms on every brief network blip and misses slow burns — an error rate of 1 in 2000 sustained for two days will eat a 99.9% monthly budget without ever crossing the threshold. The Google SRE Workbook's multi-window/multi-burn-rate alerting is the corrected pattern.
Burn rate is the speed at which budget is being consumed, expressed as a multiple of the steady-state rate. If the SLO is 99.9% and the current observed error rate is 1%, the burn rate is 1% / 0.1% = 10 — burning at 10× steady, which exhausts a 30-day budget in 3 days. The alert fires not on the current error count but on the current burn rate; the budget itself is the threshold.
Worked example: a multi-burn-rate SLO alert walked end to end
The service has a 99.9% availability SLO over a 30-day window. Compute the moving parts.
Budget. 0.1% of 30 days. 30 × 24 × 60 = 43,200 minutes. Budget = 43.2 minutes of allowed badness across the month. In events, if the service handles 100M requests/month, the budget is 0.001 × 100M = 100,000 failed requests.
Burn rate. Burn rate is current observed error rate divided by SLO error rate. Steady state at exactly 99.9% means burn rate = 1×. If the service is suddenly returning 1.44% errors, burn rate = 1.44 / 0.1 = 14.4×.
Threshold derivation. At 14.4× burn, the entire 30-day budget exhausts in 30 / 14.4 ≈ 2.08 days. So if 14.4× burn sustains for 2 days, the budget is gone. The Google SRE Workbook recipe picks this rate as the fast-burn threshold — it represents "spending 2% of the monthly budget in 1 hour" — and pairs it with a 1-hour long window and a 5-minute short window. Both windows must exceed 14.4× for the page to fire.
Similarly, 6× burn exhausts the budget in 30 / 6 = 5 days. Paired with a 6-hour long window and a 30-minute short window, this catches a slow leak that the fast threshold would miss.
The recipe — two alerts.
Alert A (fast burn):
expr: rate_1h > 14.4 AND rate_5m > 14.4
for: 0 minutes
severity: page
Alert B (slow burn):
expr: rate_6h > 6 AND rate_30m > 6
for: 0 minutes
severity: page
Why two windows on each alert? The long window suppresses flaps — a 30-second outage will not move a 1-hour rate over 14.4× unless the service was already burning. The short window suppresses stale pages — once the operator fixes the issue, the 5-minute rate falls below threshold immediately and the page clears even if the 1-hour rate is still elevated. Together they give a near-instant signal on a real fast burn and a clean reset when the problem stops.
Why two alerts together? The fast alert catches outages — a bad deploy returning 5% errors crosses 14.4× in seconds and pages quickly. The slow alert catches leaks — a flaky dependency producing 0.7% errors burns at 7× and the fast alert never fires, but the slow alert catches it within tens of minutes.
What the on-call sees. Alert A fires at 14:02 with text "Service X burning error budget at 14.4× — fast burn — 1h: 18×, 5m: 22×." The on-call opens the runbook, sees a recent deploy, rolls back at 14:09. The 5-minute rate falls to baseline by 14:14; alert clears at 14:14. Time-to-recovery: 12 minutes. Budget spent on this incident: 14.4× × 7 minutes (the burn-only portion) ≈ 100 minutes-equivalent, which is about 2.3% of the monthly budget. The team still ships features this week.
Tuning. If the page count from a quarter is too high, the windows are too short or the thresholds too low. If incidents are landing without paging, the windows are too long or the thresholds too high. Tune on the post-quarter review, not in the middle of an incident.
The trade-off in SLO alerting is between speed and noise. Faster paging on smaller windows catches problems earlier and pages more often on transient noise. Slower paging on bigger windows trades a few minutes of detection latency for cleaner pages. The Workbook recipe — two alerts, four windows — is the well-tuned middle. The deeper trade-off is the SLO target itself. 99.9% is achievable for most services; 99.99% requires multi-region replication, automated failover, and a no-deploy-Friday culture; 99.999% is the realm of telcos and aviation, with the cost structure to match. Pick the target you can actually defend with the team's current operational practice, not the target the customer asks for in the procurement meeting.
The contract dimension is what makes SLOs work as engineering tools. If management overrides the change freeze when the budget is exhausted, the SLO is a dashboard, not a contract. Document the policy — what happens at 25% budget, at 0%, at negative — and get the document signed at the level above engineering. Without that, multi-burn-rate alerting is a sophisticated noise generator.
Chaos engineering
A system that has never failed in production has merely not failed yet. The set of failure modes is enormous: network partitions, slow disks, leaking memory, clock skew, partial DNS resolution, a single region's API hitting a throttling cliff. No design review enumerates all of them. Chaos engineering is the practice of injecting controlled failures into a running system to discover the failure modes before users do — not because failures are rare but because the runbook for failures that have not happened is fiction until someone tries to use it.
The Netflix origin (Chaos Monkey, 2010; Greg Orzell and Cory Bennett) inverted the framing. Instead of writing tests for known faults, Chaos Monkey killed random EC2 instances during business hours, every business day. The discipline forced the team to assume any single instance could disappear at any time and design the system accordingly. Chaos Kong extended it to a whole AWS region; the Simian Army added latency, malformed packets, and dependency outages.
The current shape comes in two flavours. Game days are scheduled exercises — pick a hypothesis, pick a fault, run it, observe, debrief. A team picks a quiet Wednesday afternoon, injects 500 ms latency into the payment service for 10 minutes, watches the SLO dashboard and the customer-success Slack, and documents what fired, what didn't, and what surprised them. Game days are cheap, high signal, and require explicit calendar time.
Continuous chaos runs faults all the time, sampled and bounded. Chaos Monkey, scaled-down: every weekday in working hours, kill one pod somewhere. The cost is the team must build the resilience first; the value is the system stays resilient because the chaos process keeps probing. Most teams adopt game days first, then graduate to continuous chaos for the failure modes they've already engineered around.
The fault taxonomy is finite enough to enumerate. Network: latency, packet loss, partition, DNS failure. Resource: CPU starvation, memory pressure, disk fill, file-descriptor exhaustion. Dependency: a downstream service returning errors, slowing, throttling. Application: a process killed by SIGKILL, an OOM, a config reload that fails. Infrastructure: an availability-zone outage, a region outage, a cloud control-plane outage. Chaos Mesh and Litmus (both CNCF, Kubernetes-native) inject most of these declaratively. Gremlin offers them as a hosted service. AWS Fault Injection Service is the cloud-vendor option, integrated with IAM.
The discipline is the blast radius. Every experiment must answer: what is the smallest scope where this fault is useful? What is the time budget? What is the abort condition? Start in pre-prod with a 1-pod blast radius; expand to a single AZ in production for a fixed duration; only once the resilience is established do you run continuous chaos against the whole fleet. A chaos experiment without bounds is an incident with a calendar invite. The goal is to discover the failure mode without becoming the incident.
The value is rarely the fault itself; it is the runbook gap. The team thought retries handled the downstream slowness; the experiment shows retries pile up against a thundering herd and make it worse. The team thought the read replica handled the read failover; the experiment shows the application's connection pool has no DNS TTL and pins to the dead replica for 5 minutes. The team thought the autoscaler handled the spike; the experiment shows the cold start needed a warm pool. Each finding ships as code (a fix), as documentation (an updated runbook), and as a regression test (a continuous-chaos schedule that re-runs the experiment).
The trade-off is risk. Chaos experiments do cause user-visible badness — if the engineering wasn't already there, the experiment finds out by breaking production. Run during business hours so the team is awake; start small; have a stop button; do not run chaos at midnight on a holiday. Done well, chaos engineering converts unknown failure modes into known ones one at a time. Done badly, it is a fast way to learn the postmortem template.
Disaster recovery
A backup is a file; a disaster is a working service. Disaster recovery is the engineering that gets the service back online when a backup is the only thing left. The hard part is not the file. It is the database schema you had not migrated to the warm replica, the secret-manager lease that was scoped to the old region, the DNS that still routes to the old endpoint, the client retry loop that won't trust a server with a stale certificate. DR is a system property, not an artefact.
Two numbers anchor the contract. RPO (Recovery Point Objective) is the maximum acceptable data loss measured in time — "we can lose at most 1 minute of writes." RTO (Recovery Time Objective) is the maximum acceptable downtime — "we can be unavailable at most 5 minutes." RPO is determined by the replication strategy; RTO is determined by the failover orchestration plus everything that has to wake up downstream of the failover. Both numbers cost money; the lower the number, the higher the recurring infrastructure spend.
The topology choices are a spectrum. Active-passive keeps a standby region or cluster up but not serving — replication runs into it, DNS or load-balancer config points only to the active side, and failover is the operator action of flipping the pointer. Cheap to operate, slow to fail over. Active-active runs both sides simultaneously; clients are routed to either by geographic load balancing or anycast, and the failure of one side reduces the cluster from N to N-1 without a flip. Fast to fail over, expensive to operate, hard to engineer because writes have to converge. Cell-based partitions the customer population into independent cells (a cell is a complete stack — service, database, queue, cache) and routes each customer to their cell; failure of one cell affects only that cell's customers, and the blast radius is bounded by the cell size.
The replication math sets RPO. Synchronous replication acknowledges a write only after both sides have it; RPO = 0 in the limit, but every write pays the cross-replica round trip in latency — typically 10–60 ms for cross-region. Synchronous quorum replication (Spanner, CockroachDB, etcd) waits for a majority of replicas, balancing durability against the slowest replica's tail. Asynchronous replication streams the write log to followers without waiting; RPO = current replication lag, which is normally seconds but blows out under load to minutes or hours. The honest answer is: pick the replication shape that matches the RPO you promised; do not promise an RPO the replication shape cannot support.
Worked example: a multi-region failover sequence with the minutes counted
The service runs active-passive across us-east-1 (primary) and eu-west-1 (standby). The promise is RPO = 1 minute, RTO = 5 minutes. The primary fails. Walk the sequence.
t=0:00 — failure begins. The primary database leader becomes unreachable. Application requests start timing out at 30 seconds (default TCP). Synthetic monitors from outside the cloud begin reporting 5xx.
t=0:30 — detection. Three external health checks have to fail in succession to avoid a flap. Each check runs every 10 s; three failures take 30 s. The health-check system trips, the alarm fires, the on-call gets paged.
t=1:00 — decision. The on-call opens the runbook. The runbook says: confirm primary is dead (not just slow), confirm standby is healthy, confirm replication lag is within RPO, then trigger failover. If the on-call is well-trained and the dashboard is clear, this takes 1 minute. If anything is ambiguous, this can take 10. The decision step is the most failure-prone in the sequence; well-run shops automate it conditionally on hard signals (the cloud's own AZ health bit, the database's own quorum loss).
t=1:30 — promotion. The standby database is promoted to leader. For a Postgres streaming replica, this is pg_promote() plus a configuration flip on application clients. For a Raft cluster like CockroachDB, the surviving majority elects a new leader automatically and this step happens at t=0:30. For Spanner-class infra, it happens before the alarm fires. Promotion duration: 30 s to 2 min depending on stack.
t=2:00 — DNS / load-balancer cut. Two common shapes. DNS failover flips the A or CNAME record at the DNS provider; the TTL is the bound on client adoption — a 60 s TTL means clients pick up the new endpoint over the next 60 s, though some misbehaved clients cache forever. Anycast / global LB (CloudFront, AWS Global Accelerator, GCP global LB) flips the routing inside the cloud edge in seconds without DNS at all. Anycast wins on speed; DNS is universal. For the runbook assume DNS TTL = 60 s, so traffic is fully drained from the old region by t=3:00.
t=3:00 — data-plane reconvergence. Replication lag at the moment of failure was 8 seconds — better than RPO promise. The 8 seconds of writes that hadn't replicated are lost. The application needs to know which writes were on the lost side and either replay them (if the source — say an upstream Kafka — has them) or accept the loss. Some writes will be replayed by clients via idempotent retries; idempotency keys on every write are what make this safe — without them, a retry doubles the charge.
t=3:30 — warm the new primary. The cache in the new region is cold. The first thousand requests hit the database. The database's cache is cold. p99 latency is 3× normal for the first 5 minutes. The autoscaler in the new region adds instances if the load is higher than the per-instance capacity. A pre-warmed standby (a fleet that runs continuously, just not taking traffic) shortcuts this step; a cold standby pays the full cost.
t=4:30 — service nominal. Synthetic monitors report green. Internal SLO dashboard shows recovery. The on-call posts the all-clear in the incident channel. RTO realised: 4 minutes 30 seconds. Below the 5-minute promise.
RPO and RTO realised, summed. RPO = 8 s (the unreplicated tail at the moment of failure). RTO = 4:30. Both under target. Note that this required: a written runbook the on-call could execute, a standby that was healthy at the moment of need, replication that was within its lag budget, DNS TTLs set short, idempotency keys on writes, and a single on-call who knew the stack. Any one of those missing, and the realised numbers double or triple.
Cost of the promise. Running an active-passive cross-region setup costs roughly 1.7× the cost of a single region (the standby is provisioned but mostly idle). Active-active costs roughly 2.0× (both sides serve, but you carry redundancy on each). The savings against the cost of one bad outage is the trade — for a customer-facing payment system, the math usually favours active-active; for an internal admin tool, the math favours nightly snapshots and the patience to restore.
The cross-act dependency runs deep. The replication shapes here come from Act Vb — sync quorum, async leader-follower, conflict-free replicated types (CRDTs) for active-active write convergence. The DNS and anycast machinery comes from Act Va. The architecture choice (cell, region, AZ) comes from Act Vc. DR is the integration test for all three: when the disaster comes, every assumption upstream becomes visible.
The trade-off is brutal honesty about scope. Most teams cannot afford active-active with RPO = 0; most also cannot afford a four-hour outage. The compromise is active-passive with automated failover at RPO of seconds and RTO of minutes, validated by a quarterly failover drill. The quarterly drill is the only thing that keeps the runbook from being fiction — staff turnover and dependency drift will rot it otherwise.
Incident response
Production breaks. The question is not whether, but how the team behaves in the next 30 minutes. Incident response is the operational practice that turns "everything is on fire" into "this is severity 2, the commander is named, mitigation is in progress, comms go out every 15 minutes." The structure is what makes the time pressure survivable.
Severity scales standardise the urgency. A common four-level scheme: SEV1 is a full outage or data-integrity event affecting many users — pages everyone, wakes leadership, runs a full incident process. SEV2 is a major impairment — significant user impact, key feature broken, pages the on-call and the team lead. SEV3 is a minor impairment — degraded experience for some users, runs during business hours. SEV4 is a self-healing or cosmetic issue — gets a ticket, not a page. The scale matters because it sets the response: who is paged, what channels open, how often comms go out, who has authority to make customer-facing statements.
The incident commander (IC) is a named role, not a seniority. The IC owns the incident — coordinates the responders, runs the comms cadence, decides when to escalate, decides when to declare resolved. Critically, the IC is not debugging — somebody else does the technical work. A senior engineer trying to both fix the bug and run the comms will do neither well. The IC role rotates with the on-call; everyone in the rotation should be trained to run it.
The first ten minutes are the part that benefits most from a checklist. Page fires; on-call acknowledges within 5 minutes (or it auto-escalates). Open a named channel — #inc-2026-05-15-checkout-500s — to centralise the conversation. State the symptom in one sentence and pin it. Assign IC, technical lead, comms lead (in a small team these are one person initially; split as soon as more hands arrive). Start mitigation. Send the first external update. The discipline that matters: mitigate before resolve. A bad deploy gets rolled back before anyone debugs why it was bad. A feature flag gets turned off before the team understands which interaction caused the spike. The root cause is the postmortem's job, not the incident's.
Comms are templates, not prose. The external update at t+8 minutes is two sentences: "We are investigating an issue affecting checkout. Updates every 15 minutes." Not "We have identified a problem with the checkout service that appears to be related to a recent deployment of …" — that paragraph requires the team to know the root cause, which they don't yet, and writing it slows the mitigation. Templates relieve the cognitive load: pick the template, fill the two blanks, post. Internal updates are similarly structured: a timeline post per significant event, an at-mention to anyone whose attention is now required.
Decision-making under pressure has known patterns. The on-call is sleep-deprived and tunnel-visioned; the IC's job is to widen the lens. Useful prompts: "What else could it be?" (force consideration of an alternative hypothesis), "What's the worst that happens if we roll back?" (force a comparison between mitigation actions), "Who else should be on this?" (force escalation before the situation deteriorates). The opposite of a useful prompt is "Who broke this?" — the room shuts down, comms slows, the bug hides.
The blameless postmortem (Act IXb covered the basic shape) deepens with practice. The five whys, done honestly, descends from the surface fact to the systemic. The discipline is to not stop at the first plausible answer. "The migration locked the orders table" is a code fact, the first why. "The migration ran a backfill in one transaction" is a design fact, the second why. "The engineer copied a pattern from a smaller table's migration" is a team fact, the third why. "The migration template doesn't distinguish hot from cold tables" is a tooling fact, the fourth why. "No review step exists for schema changes against high-QPS tables" is a process fact, the fifth why. Action items attach to the process fact, because that's the level at which changing one thing prevents the recurrence.
Done as theatre, the five whys is five rephrasings of the surface fact and the action item is "be more careful." Done honestly, it produces tickets that real owners can ship: add a pre-flight check to the migration tool, add a review gate in the schema-change workflow, add a chaos drill that runs a backfill against a hot table to catch the next instance before it ships.
On-call rotation design has humane defaults. Follow-the-sun across time zones is the gold standard for large teams — primary on-call sits in the working hours of one region, hand-off at end of region day, no one wakes up at 03:00 if avoidable. A small team can't do follow-the-sun and must accept night pages; the mitigations there are a primary/secondary structure (the secondary catches the page the primary missed), a stand-down day after a busy night, and a rotation length short enough that nobody is on call for two weeks straight. The PagerDuty research is consistent: a humane on-call has page volume under 2/week and night pages under 2/quarter. Above those, retention collapses, and the next outage is investigated by people who weren't around for the last one.
The trade-off in on-call is between coverage and burnout. Tighter SLOs and broader scope page more often; the cost is paid in turnover. The honest move is to measure page volume, treat sustained pager noise as a bug (sometimes in the alert, sometimes in the service), and budget engineering time to fix the noisiest alerts each quarter. Page humaneness is an operational metric like latency — instrument it, alert on it, defend it. The team that runs production has to live to run it next year.
Standards
The operations layer has more written-down practice than almost any other corner of software engineering — Google published two books, the CNCF runs working groups for telemetry and chaos, and the cloud vendors publish opinionated playbooks. The list below is the canonical reading for the practitioner who wants the depth.
Foundational SRE texts:
- Google SRE Book — Site Reliability Engineering, Beyer, Jones, Petoff, Murphy (O'Reilly, 2016). The first formal articulation of SLO/error-budget mechanics, on-call practice, and the SRE-as-discipline framing. Free online at sre.google/books.
- Google SRE Workbook — The Site Reliability Workbook, Beyer, Murphy, Rensin, Kawahara, Thorne (O'Reilly, 2018). The companion of recipes — multi-burn-rate alert math, error-budget policy examples, postmortem templates.
- Google Building Secure & Reliable Systems — Building Secure and Reliable Systems, Adkins et al. (O'Reilly, 2020). The integration of security with SRE practice.
Observability and telemetry:
- OpenTelemetry specification —
opentelemetry.io/docs/specs/otel/. The CNCF specification covering the trace, metric, and log data models, the OTLP wire protocol, and the SDK behaviour contract. The vendor-neutral instrumentation standard. - W3C Trace Context —
w3.org/TR/trace-context/. The W3C Recommendation defining thetraceparentandtracestateHTTP headers that propagate span context across service boundaries. - Prometheus —
prometheus.io/docs. The de facto open-source metrics stack — exposition format, query language (PromQL), and alertmanager rule shape. - Grafana —
grafana.com/docs/. The dashboard and alerting layer most teams use on top of Prometheus, Loki (logs), and Tempo (traces). - OpenMetrics —
openmetrics.io. The formalised successor to the Prometheus exposition format, now a CNCF specification. - Brendan Gregg's USE method —
brendangregg.com/usemethod.html. The Utilisation/Saturation/Errors checklist for systems-performance investigation. - Brendan Gregg's off-CPU analysis —
brendangregg.com/offcpuanalysis.html. The framework for finding latency in time spent blocked rather than time spent computing. - eBPF documentation —
ebpf.io. The kernel-level probe technology underpinning current continuous-profiling and observability tooling.
Continuous profiling:
- pprof —
github.com/google/pprof. The Go-pioneered sampling-profile format and viewer; the substrate for many continuous-profiling backends. - Pyroscope —
pyroscope.io. Open-source continuous-profiling platform, now part of Grafana Labs. - Parca —
parca.dev. Open-source eBPF-based continuous profiler with a pprof-compatible data model. - CNCF Continuous Profiling working group —
github.com/cncf/tag-observability. The cross-vendor effort to standardise profile collection and storage.
Queueing theory and performance:
- Gil Tene's coordinated-omission notes — "Latency Tip of the Day", Tene. The talks and notes that named and explained coordinated omission; required reading before publishing any latency number.
- HdrHistogram —
hdrhistogram.org. The high-dynamic-range histogram data structure (Tene) that captures latency with constant accuracy across many orders of magnitude. - Brendan Gregg, Systems Performance — Systems Performance, 2nd edition (Addison-Wesley, 2020). The reference for Linux performance methodology, with sections on CPU, memory, I/O, network, and the USE method.
- Neil Gunther, PDQ and USL — Guerrilla Capacity Planning. The applied queueing-theory work behind the Universal Scalability Law and the PDQ analytical tool.
Chaos engineering and resilience:
- Principles of Chaos Engineering —
principlesofchaos.org. The community manifesto defining the discipline (hypothesise, run in production, minimise blast radius, automate continuously). - Netflix Chaos Monkey —
netflix.github.io/chaosmonkey/. The original tool and its design philosophy. - Chaos Mesh —
chaos-mesh.org. CNCF-incubating, Kubernetes-native fault-injection platform. - Litmus —
litmuschaos.io. CNCF-graduated chaos-engineering framework with a community experiment hub. - Gremlin —
gremlin.com. Hosted chaos-engineering platform; their Chaos Engineering book (Rosenthal & Jones, O'Reilly, 2020) is a practitioner reference.
Disaster recovery:
- AWS Well-Architected Reliability Pillar —
docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/. The opinionated cloud playbook for multi-AZ and multi-region resilience; the RPO/RTO categories (backup-restore, pilot-light, warm-standby, multi-site active-active) are the standard taxonomy. - ISO 22301 — Business Continuity Management Systems. The international standard for business-continuity programmes; cited in audits and procurement for any regulated buyer.
- NIST SP 800-34 Rev. 1 — Contingency Planning Guide for Federal Information Systems. The U.S. reference for IT-contingency planning; widely adopted outside government as a baseline.
- Google Spanner / TrueTime papers — Corbett et al., "Spanner: Google's Globally-Distributed Database". The reference architecture for synchronous global replication with bounded clock skew.
Incident response:
- PagerDuty Incident Response documentation —
response.pagerduty.com. The most accessible open-source incident-response playbook, with role definitions, severity scales, and runbook templates. - Google SRE Workbook, ch. on incident response — see Foundational texts above. The Workbook's incident-response chapter is the formal articulation of the IC/technical-lead/comms-lead split.
- Etsy Debriefing Facilitation Guide — Allspaw, "Etsy Debriefing Facilitation Guide". The reference for running an honest, blameless postmortem; treats the debrief as a learning event rather than a forensic one.
- Atlassian Incident Handbook —
atlassian.com/incident-management/handbook. A second perspective with concrete role definitions and comms templates.
Load-testing tooling:
- k6 —
k6.io/docs/. Open-source load generator; open-loop by default, JavaScript test scripting. - wrk2 —
github.com/giltene/wrk2. Gil Tene's fork of wrk that fixes coordinated omission; the reference open-loop HTTP load generator. - Vegeta —
github.com/tsenart/vegeta. HTTP load testing with a constant-rate request model and detailed latency reporting. - JMeter —
jmeter.apache.org. The long-standing Apache load-testing tool; flexible but closed-loop by default — configure with care if you need an honest tail.
Cross-act references:
- Act IV for the OS-level reality of process scheduling, memory, file descriptors, and I/O. The autoscaler can add instances, but a single process's CPU profile is constrained by how the kernel schedules it.
- Act Va for the network reality under load — TCP retransmits, head-of-line blocking, DNS TTLs, anycast — that determines what failover and load balancing can actually deliver.
- Act Vb for the replication and consensus mechanisms (Raft, Paxos, quorum, CRDTs) that DR plans rest on. RPO is a property of the replication shape, not a number you can set independently.
- Act Vc for the architectural choices — cell-based, microservices, event-driven, service-mesh — that determine what's possible at the operations layer. You cannot operate your way out of a poorly-decomposed monolith.
- Act IXb for the broader engineering-craft frame: version control, code review, testing, CI/CD, and the overview pass of SLOs and postmortems that this page goes deeper than.
- Act IXa for the user-perceived consequences of operations work. A 5-minute RTO is a number engineers track; the user sees a checkout retry succeed where it would have failed.
Branches that earn their own article.
- eBPF for production observability.
- Continuous profiling at scale (Pyroscope, Parca).
- OpenTelemetry deep dive (collector, exporters, sampling).
- Prometheus storage internals and PromQL.
- Service-level objective math (burn-rate alert formulas).
- Chaos Mesh / Litmus / Gremlin tooling.
- Multi-region database topologies.
- Game-day playbooks.
- On-call rotation design and humane scheduling.
- Postmortem culture and writing.