41 KiB
Troubleshooting Reference for Site Reliability Engineering
Table of Contents
- Introduction
- The Hypothetico-Deductive Method
- Common Pitfalls
- Problem Report Format
- Triage Prioritization
- Diagnostic Tools
- Generic Debugging Techniques
- Experimental Design
- The Value of Negative Results
- Case Study: App Engine Latency
- Building for Observability
- Troubleshooting Expertise: Systematic vs Experience-Based
- References and Further Reading
Introduction
Troubleshooting is the art and science of diagnosing and resolving failures in complex systems. In Site Reliability Engineering (SRE), troubleshooting is not merely a reactive activity — it is a structured discipline that combines systematic reasoning, domain knowledge, empirical observation, and the careful design of experiments. Google's SRE teams, as documented in the SRE books, treat incidents as learning opportunities and invest heavily in the tools, practices, and culture that enable rapid diagnosis.
This reference distills the core troubleshooting methodology used by SRE practitioners into a practical guide. It covers the scientific method adapted for production systems, common cognitive traps, diagnostic tooling, experimental design, and the architectural patterns that make troubleshooting easier in the first place.
The target audience is an SRE agent operating on production systems: the methods described here should inform every investigation from a pager alert to a postmortem deep-dive.
The Hypothetico-Deductive Method
At its core, troubleshooting is hypothesis-driven inquiry. The hypothetico-deductive method — borrowed from the scientific tradition — provides a rigorous framework:
The Cycle
-
Observe the failure. Collect baseline data from monitoring, alerting, logs, and user reports. Establish what is actually happening, not what you assume is happening.
-
Form one or more hypotheses. A hypothesis is a testable explanation for the observed failure. It must be falsifiable — capable of being proven wrong by an experiment. Example: "The increased latency is caused by CPU throttling due to a co-located noisy neighbor on the same hypervisor."
-
Design an experiment. The experiment must produce a clear result that either supports or refutes the hypothesis. It should isolate one variable at a time. Experiments can be:
- Observational: Examine metrics, logs, or traces for evidence that supports or contradicts the hypothesis. "Check if CPU throttling counters are elevated on the affected instances."
- Interventional: Make a controlled change and observe the outcome. "Migrate one affected pod to a different node and measure whether latency returns to baseline."
- Reproduction: Attempt to recreate the failure under controlled conditions. "Run the same request pattern against a staging environment with simulated CPU throttling."
-
Execute the experiment and collect results. Run the experiment cleanly — do not contaminate it with other simultaneous changes.
-
Interpret results. Did the outcome support or refute the hypothesis?
- Supported: The hypothesis remains viable. Proceed to refine it, make predictions, and test further.
- Refuted: The hypothesis is invalid. Discard it and form a new one.
- Inconclusive: The experiment was poorly designed or the data is noisy. Redesign and retest.
-
Iterate. Repeat steps 2-5 with increasingly refined hypotheses until the root cause is identified.
-
Confirm root cause. Once you believe you have found the root cause, verify by:
- Applying a fix and observing that the failure clears.
- Reverting the fix and observing that the failure returns.
- Ideally, reproducing the failure again from scratch.
The Importance of Falsifiability
A non-falsifiable hypothesis is a liability. "The system is slow because of some networking issue" cannot be disproven by any experiment — it is therefore useless. A good hypothesis is specific enough to fail: "The system is slow because the database connection pool on host-42 is exhausted, causing request queuing at the middleware layer."
Common Hypothesis Categories
When forming your initial hypotheses, draw from the USE method (Utilization, Saturation, Errors) adapted for system resources:
- Utilization: Is a resource being heavily used? CPU, memory, disk I/O, network bandwidth, file descriptors, connection pools.
- Saturation: Is a resource over-subscribed? Run queue length, memory pressure, disk queue depth, TCP backlog.
- Errors: Is a component producing errors? Application error rates, system call failures, dropped packets, timeouts.
For distributed systems, also consider:
- Configuration drift: Did a config change roll out that altered behavior?
- Dependency failure: Is an upstream or downstream service degrading?
- Traffic pattern change: Did the request mix shift (e.g., more expensive queries)?
- Resource exhaustion: A subtle depletion over time (e.g., connection leak, memory leak, disk filling).
- Latency amplification: A small slowdown in one service cascading due to retries or queuing.
Common Pitfalls
Even experienced SREs fall into predictable traps. Awareness of these patterns reduces diagnostic time and prevents wasted effort.
1. Anchoring on Irrelevant Symptoms
The trap: The first observation you make becomes an anchor, and you interpret all subsequent data to confirm it.
Example: A service is returning 503 errors. You notice disk I/O is high and spend an hour investigating I/O patterns. The actual cause was a misconfigured load balancer that dropped backend connections — the disk I/O was a benign background process.
Mitigation: Generate at least three distinct hypotheses before running any experiment. Explicitly write down what evidence would refute your leading hypothesis.
2. Confusing Correlation with Causation
The trap: Two metrics move together; one is assumed to cause the other.
Example: Error rate rises at the same time memory usage increases. You investigate memory leaks for two hours. The actual cause was a new deployment that introduced a bug — the memory increase was due to the new code path allocating more objects, not the cause of the errors.
Mitigation: Correlation generates hypotheses, not conclusions. Design an experiment that isolates the putative cause. If you cannot manipulate the cause independently, you cannot prove causation.
3. Logical Fallacies
Common fallacies that derail troubleshooting:
- Confirmation bias: Seeking only evidence that confirms your hypothesis. Actively seek disconfirming evidence.
- Base rate neglect: Ignoring how common a failure mode is. The most common cause is often the simplest — check recent deploys, config changes, and dependency health first.
- The Texas sharpshooter fallacy: Drawing a target around where the arrows landed. "All five failed requests came from the East region, therefore the East region is the problem." But if you had 1000 requests, five failures in one region may be random.
- False cause (post hoc ergo propter hoc): "We restarted the database, and the error rate dropped, so the database was the problem." But the error rate may have dropped due to a traffic lull or a concurrent remediation.
4. Unsafe Testing in Production
The trap: Running experiments that risk customer impact or data loss.
Examples:
- Restarting a production database without understanding the cache warm-up cost.
- Killing a process to test auto-recovery in a stateful service.
- Changing firewall rules without a rollback plan.
Mitigation:
- Prefer observational experiments (metrics, traces, logs) over interventional ones when possible.
- When intervention is necessary, start with the smallest possible blast radius (one instance, one request type, one region).
- Always have a rollback plan before applying any change.
- Use canary analysis and gradual rollouts.
5. Premature Closure
The trap: Stopping the investigation once a plausible explanation is found, without verifying it is the actual root cause.
Example: High latency is "solved" by restarting a service that had memory pressure. The latency returns a week later because the actual root cause — a connection leak — was never fixed.
Mitigation: Always verify root cause by reproducing the failure from scratch or by confirming that the fix eliminates the symptom and reverting it brings the symptom back.
6. Fixing Symptoms Instead of Root Causes
The trap: Restarting a process, clearing a cache, or bumping a resource limit solves the immediate problem but does not address why the problem occurred.
Mitigation: Apply a tactical fix to stop the bleeding, then conduct a root cause analysis to find and fix the underlying issue. Distinguish between "mitigation" and "resolution."
7. The "It Worked Before" Fallacy
The trap: Assuming that because a change or configuration worked in the past, it cannot be the cause of a new failure.
Example: "We've been using the same database driver for two years, so it can't be the problem." But a new query pattern, new data volume, or new driver version may expose a latent bug.
Mitigation: No assumption is safe. Treat every component as a potential suspect, especially when the system state has changed elsewhere.
Problem Report Format
Clear problem reporting is the foundation of effective troubleshooting. When an incident is reported — whether by a monitoring system, a user, or a fellow engineer — the report should contain three essential elements:
1. Expected Behavior
A precise statement of what the system should be doing. Without this, you cannot determine whether a deviation is an actual problem.
- Bad: "The site is down."
- Good: "The service should respond to HTTP GET /api/orders with a 200 status code and a JSON array containing the user's orders within 500ms p99 latency. Currently, 85% of requests are returning HTTP 503."
Include the source of the expectation: an SLO, a previous baseline, a documented specification, or a contract.
2. Actual Behavior
The observed deviation. Quantify everything possible.
- Scope: Does it affect all users? A subset? One region? One instance?
- Severity: What is the actual impact? Requests failing? Slow but succeeding? Data loss?
- Timeline: When did it start? Is it continuous or intermittent? Does it correlate with any known events?
- Trend: Is it staying constant, growing, or shrinking?
3. Reproduction Steps
How to observe the failure independently. This is critical for both verification and postmortem.
- "curl -w '%{http_code}\n' https://service.example.com/api/health returns 503."
- "Send 1000 requests of type X from region Y using our load-testing framework; observe p99 latency > 2s."
- "Check the error rate dashboard at [link]: service_A.errors.5xx shows a 12% rate since 14:32 UTC."
Template
## Problem Report
### Expected Behavior
[What should happen, including SLOs or baselines]
### Actual Behavior
[What is happening instead, with quantified data]
### Scope
- Affected components:
- Affected regions/zones:
- User impact:
- Time window:
### Reproduction
1. [Step 1]
2. [Step 2]
3. [Step 3]
### Initial Diagnostics
[Any first-pass data already collected: dashboards checked, logs reviewed, etc.]
Triage Prioritization
When an incident arrives, the first priority is containment — not root cause analysis. The triage order is:
Stop the Bleeding First
-
Assess impact and severity. What is the blast radius? Are users affected? Is data at risk? Use the priority matrix:
- P0: Critical — service down, data loss, security breach. Immediate response.
- P1: Major — degraded service for many users. Respond within minutes.
- P2: Minor — degraded service for a subset of users, or cosmetic issues. Respond within hours.
- P3: Cosmetic — low-impact issues. Respond when convenient.
-
Apply immediate mitigation. The goal is to restore service, not to find the root cause. Mitigation options (in roughly this order):
- Roll back a recent deployment.
- Fail traffic over to a healthy region or replica.
- Scale up capacity (add instances, increase resource limits).
- Restart a service (with appropriate caution for stateful services).
- Block malicious or problematic traffic.
- Enable a feature flag or circuit breaker to disable a suspect feature.
-
Verify mitigation. Confirm that the mitigation actually restored the service. Monitor the recovery trend, not just the current value.
-
Tag the incident for follow-up. Create a tracking issue or incident record. Note what mitigation was applied and what data was collected before the mitigation (pre-mitigation state is invaluable for root cause analysis).
-
Conduct root cause analysis. Only after the immediate crisis is contained should you switch to thorough investigation. Use the hypothetico-deductive method described above.
The "5 Whys" in Triage
During triage, shallow versions of the "5 Whys" can be useful — but only to find a mitigation, not to declare a root cause.
- Why are we paging? > Error rate is high.
- Why is error rate high? > Database queries are timing out.
- Why are they timing out? > Connection pool is exhausted.
- Why is it exhausted? > Connections are not being returned to the pool after queries.
Mitigation: Restart the service to clear the pool. Root cause investigation follows.
When NOT to Mitigate Immediately
There are cases where immediate mitigation is harmful:
- The mitigation itself is risky (e.g., restarting a database with slow recovery).
- The failure is self-correcting and the mitigation would destroy forensic evidence.
- The scope is narrow and the cost of investigation is lower than the cost of mitigation.
Use judgment: if 100 users are seeing an intermittent error and you can collect traces while they do, the investigation may be more valuable than a blind restart.
Diagnostic Tools
Modern production systems generate vast amounts of telemetry. The SRE must know what tools are available and, more importantly, when to use each one.
1. Monitoring and Alerting (The First Look)
- Dashboards: High-level service health. Look for anomaly patterns: spikes, step-changes, flatlines, slow drifts.
- Alerting: Rules that fire when SLO burn rates exceed thresholds. Alerts should tell you what is wrong, not just that something is wrong.
- Red/Green metrics: At a glance, is the service red (failing) or green (healthy)? Instrument four golden signals: latency, traffic, errors, saturation.
When to use: Always — this is the starting point for any investigation.
2. Logging (The Narrative)
- Structured logs: JSON-formatted log entries with consistent fields: severity, service, trace_id, request_id, user_id, latency, status_code.
- Log aggregation: Centralized logging systems (ELK, Loki, BigQuery). Query by trace_id to reconstruct a single request's journey.
- Log levels: DEBUG, INFO, WARN, ERROR, FATAL. During active troubleshooting, temporarily increase log verbosity on affected instances (via runtime config, not redeployment).
When to use: When you need to see the detailed behavior of individual requests or components. Essential for understanding error paths.
3. Distributed Tracing (The Flow)
- Trace spans: Each unit of work (RPC, DB query, cache lookup) produces a span with start time, duration, and metadata.
- Trace context: Propagated via HTTP headers (traceparent, x-cloud-trace-context). Must be passed across service boundaries.
- Sampling: Head-based (consistent) or tail-based (dynamic). For debugging specific failures, use a higher sampling rate on error traces.
When to use: When the failure involves multiple services or when you need to understand latency breakdown across hops. Invaluable for "needle in a haystack" problems.
4. State Endpoints and Debug Interfaces
- Health checks:
/healthz,/readyz— basic liveness and readiness. - Metrics endpoints:
/metrics— Prometheus-format metrics exposed by the application. - Debug endpoints:
/debug/pprof/,/debug/vars— runtime profiling data (goroutines, heap, mutex contention). - State dumps:
/debug/state,/admin/dump— internal state for manual inspection. - Configuration endpoints:
/debug/flags,/admin/config— current runtime configuration. - Live profiling: Continuous profiling (e.g., Google's profiling agent, Pyroscope) captures CPU, heap, and mutex profiles without requiring a request to a debug endpoint.
When to use: When you need to inspect the internal state of a running process. Critical for diagnosing resource leaks, deadlocks, and configuration issues.
5. Infrastructure Tools
- Process-level:
top,htop,ps,lsof,strace,perf,bpftrace. - Network:
netstat,ss,tcpdump,curl,mtr,dig,iftop,tcptraceroute. - Disk:
iostat,iotop,df,du,dstat. - Kernel:
vmstat,sar,/proc/*,sysdig. - Container/Kubernetes:
kubectl describe,kubectl logs,kubectl exec,crictl,ctr.
When to use: When the failure appears to be at the infrastructure layer rather than the application layer — high system load, network issues, disk failures.
Choosing the Right Tool
The order of tool use often follows a narrowing funnel:
- Monitoring dashboards — identify what is wrong and when it started.
- Logs — find specific error messages or request patterns.
- Tracing — understand which service or call is the bottleneck.
- State endpoints — inspect the offending component's internals.
- Infrastructure tools — examine the host if resource exhaustion is suspected.
Generic Debugging Techniques
These techniques apply across virtually all systems and failure modes. They are the "universal tools" of the SRE troubleshooting toolkit.
Divide and Conquer (Binary Search)
The most powerful debugging technique. Narrow the search space by splitting the system in half and determining which half contains the fault.
Application to request paths:
- A request goes through: LB -> Auth -> API Gateway -> Service A -> Cache -> Service B -> Database.
- Check if the failure occurs before or after Service A. If after, the fault is in Service B, Cache, or DB.
- Check if it occurs before or after the Cache. If after, suspect Service B or DB.
- Continue halving until the culprit is isolated.
Application to time ranges:
- The failure started sometime in the last 6 hours.
- Check monitoring data from 3 hours ago. If the failure was present, divide the first 3 hours. If not, divide the last 3 hours.
Application to instance sets:
- 10 instances: 5 are failing, 5 are healthy. What is different between the two groups? Compare config, deployment version, resource allocation, node location.
Bisection (for Root Cause in Code)
When the root cause is suspected to be a code change, use git bisect to find the exact commit that introduced the regression.
git bisect start
git bisect bad # current version is broken
git bisect good <last-known-good-commit>
# git checks out the midpoint; test it
# git bisect good / git bisect bad
# repeat until the offending commit is found
Correlate Changes
Most production failures are caused by change. Correlate the failure timeline with:
- Deployments: What changed in the last N minutes/hours? Check deployment history for ALL services, not just the one failing.
- Configuration changes: Feature flags, routing rules, quota limits, firewall rules.
- Infrastructure changes: Scaling events, node upgrades, network changes, DNS updates.
- Dependency changes: External API version bumps, database schema migrations, certificate rotations.
- Traffic changes: Sudden traffic spikes, shifts in request composition, new clients connecting.
The "What Changed?" Checklist
When investigating any incident, ask these questions first:
- Was there a deployment to any service in the call path in the window before the incident?
- Was there a configuration change?
- Was there an infrastructure change (scaling, node pool update, network rule)?
- Did a dependency change (database schema, external API, certificate)?
- Did traffic patterns change (volume, request mix, client distribution)?
- Did any resource cross a threshold (disk 90% full, connection pool exhausted, rate limit hit)?
The "Breadcrumb Trail" Technique
Follow the breadcrumbs from the observable symptom backward through the system:
- Symptom: User sees "500 Internal Server Error."
- Breadcrumb 1: The load balancer logged the 500 with upstream response time > 30s.
- Breadcrumb 2: The application log shows the request timed out waiting for a database query.
- Breadcrumb 3: The database slow query log shows the query took 25s.
- Breadcrumb 4: The query plan shows a full table scan on a 100M-row table.
- Breadcrumb 5: The index used by the query was dropped by a recent migration.
Each breadcrumb is a piece of evidence that narrows the search space.
Compare to a Known Good Baseline
If you have a stale but known-good version of the system, compare its behavior:
- Compare metrics from a healthy period to the current period: what changed?
- Compare a live healthy instance to a failing instance: config, process list, resource usage.
- Compare request traces from healthy requests to failing requests: which span is different?
Work Backward from the Error
Error messages often contain the key to diagnosis:
- HTTP 503: The service is unavailable. Check capacity, health checks, load balancer configuration.
- HTTP 504: A gateway timeout. Something upstream is slow. Check tracing.
- HTTP 429: Rate limiting. Check quota usage for the client.
- TCP connection refused: The port is not listening. Check if the process is running.
- TCP connection reset: The process died or the connection was closed abruptly. Check for crashes, OOM kills, health check timeouts.
Experimental Design
Good experiments are the difference between guessing and knowing. Every experiment in an incident investigation should meet the following criteria.
Test in Order of Likelihood
Prioritize hypotheses by:
- How likely they are given prior incidents and common failure modes. Deployments and config changes are the most common causes of production failures.
- How easy they are to test. A log query that takes 10 seconds should come before a database restart that takes 10 minutes.
- How dangerous the test is. Observational tests (checking metrics) come before interventional tests (killing a process).
A reasonable order for most incidents:
- Check deployment history and config changes (2 minutes).
- Review dashboards for the four golden signals (2 minutes).
- Check dependency health (external services, databases, caches) (2 minutes).
- Examine error logs and traces for the affected requests (5 minutes).
- Check resource utilization and saturation on affected instances (5 minutes).
- Attempt reproduction in a non-production environment (10-30 minutes).
- Interventional tests with controlled blast radius (10-60 minutes).
Isolate One Variable at a Time
The cardinal rule of experimental design: change one thing at a time. If you change two things and the problem goes away, you do not know which change fixed it. If you change two things and the problem remains, you do not know whether either change had an effect.
- Bad: "We restarted the service and increased the connection pool size." Did the restart or the pool size fix it?
- Good: "We increased the connection pool size on one instance and observed no improvement. We restarted that instance and the problem resolved."
Account for Confounding Factors
A confounding factor is a variable that influences both the independent variable and the outcome, creating a spurious association.
Examples of confounders in production debugging:
- Time of day: "We restarted the service at 3:00 AM and latency dropped." But 3:00 AM is naturally low-traffic — the latency drop may be due to reduced load, not the restart.
- Coincident changes: "We rolled back the deployment and the error rate dropped." But another team also deployed a fix for an upstream dependency at the same time.
- Hawthorne effect: "We started monitoring query performance and latency improved." Simply observing a system can change behavior (engineers writing better queries because they know they are being measured).
Mitigation: Before declaring a causal relationship, ask: "What else changed at the same time?" Correlate timelines from all sources.
Control Groups and Canaries
Whenever possible, run experiments with a control group:
- Route 1% of traffic to a canary with the proposed fix, leave 99% on the current version. Compare outcomes.
- Apply a change to one instance but not its peers. Compare metrics.
- Use feature flags to enable a change for a subset of requests.
This is why progressive rollouts (e.g., 1% -> 5% -> 25% -> 100%) are standard SRE practice — they inherently provide a control group.
Reproducibility
A failure that cannot be reproduced cannot be diagnosed with certainty. If you cannot reproduce the failure in production, try:
- Replaying the exact request that failed (from logs or trace data).
- Creating an identical environment (same configuration, same dependencies, same data).
- Simulating the conditions under which the failure occurs (load, traffic pattern, resource constraints).
- Using chaos engineering tools to inject the suspected failure mode.
If a failure is truly non-reproducible, it may be a transient issue (network blip, CPU steal, cosmic ray). Document it and move on, but ensure monitoring is in place to detect recurrence.
The Value of Negative Results
In troubleshooting, negative results — experiments that refute a hypothesis — are as valuable as positive ones. Each refuted hypothesis narrows the search space.
Why Negative Results Matter
- They eliminate possibilities. Every hypothesis you can confidently reject brings you closer to the true cause.
- They prevent wasted effort. Proving that "it is not the database" prevents future investigations from revisiting the database.
- They build confidence. A thorough investigation that tested and eliminated several plausible hypotheses builds more confidence in the final root cause than one that tested only the winning hypothesis.
- They are archival. Documenting negative results in incident reports or runbooks prevents future engineers from retreading the same dead ends.
How to Document Negative Results
Every experiment should be recorded with:
- Hypothesis tested: "Hypothesis: High latency is due to database connection pool exhaustion."
- Experiment: "Checked pg_stat_activity and max_connections on the primary database instance."
- Result: "Connection count is 45 of 200, well below the limit. All connections show idle time under 100ms. Hypothesis refuted."
- Next: "Next hypothesis: High latency is due to CPU throttling on the application instances."
Avoiding "Happy Path" Thinking
When an incident is resolved, there is a natural tendency to only document what fixed it. Resist this. The dead ends and wrong turns are valuable learning material. A postmortem that says "Restarted the service" without mentioning the 45 minutes spent investigating the wrong hypothesis is a postmortem that has wasted a future engineer's time.
Case Study: App Engine Latency
This case study, drawn from Google's SRE literature, illustrates the hypothetico-deductive method in action.
The Symptom
A team running on Google App Engine noticed that request latency had been gradually increasing over the course of several weeks. The increase was subtle — p50 latency went from 100ms to 130ms, p99 from 500ms to 800ms — but it was persistent and growing.
Initial Observations
- The increase was present across all versions of the application (ruling out any single release).
- The increase correlated with the number of active versions deployed on the same App Engine instance.
- The increase was more pronounced at p99 than at p50.
- CPU utilization on the App Engine instances was flat. Memory was flat.
Hypothesis 1: Competing Versions Cause Resource Contention
Hypothesis: Each deployed version of the application consumes memory for its compiled code and runtime state. As more versions accumulate, the working set exceeds memory capacity, causing the garbage collector to run more frequently and increasing latency.
Experiment: Measure GC pause time and frequency as a function of the number of deployed versions. Deploy a new version and observe whether GC metrics change.
Result: GC pause time did increase slightly, but the increase was too small to account for the observed latency shift. The correlation between version count and latency remained even after accounting for GC time.
Conclusion: Hypothesis partially supported (some GC impact) but not sufficient to explain the full latency increase.
Hypothesis 2: Instance Scrubbing Overhead
Hypothesis: App Engine instances periodically "scrub" old versions from disk. With many versions, this scrubbing consumes I/O bandwidth, causing latency spikes for requests hitting the same disk.
Experiment: Monitor disk I/O latency and scrub activity (via logs) as version count increases. Compare I/O latency distributions for instances with few vs. many versions.
Result: Disk scrub activity was indeed higher on instances with more versions. However, the disk I/O latency distribution did not differ significantly between the two groups.
Conclusion: Hypothesis refuted.
Hypothesis 3: Connection Pool Fragmentation
Hypothesis: Each version maintains its own connection pool to the database. With more versions, the total number of connections grows, overwhelming the database's connection pool. The database itself becomes the bottleneck.
Experiment: Monitor database-side connection counts and query latency. Correlate with the number of App Engine versions deployed.
Result: Database connection count did not increase significantly. Database-side query latency was flat.
Conclusion: Hypothesis refuted.
Hypothesis 4: Request Routing Overhead
Hypothesis: The App Engine frontend (the routing layer that dispatches requests to instances) must maintain a routing table for all versions. With many versions, the routing table grows, and routing decisions take longer. The effect is most pronounced at the tails (p99) because of occasional routing table churn.
Experiment: Measure time spent in the App Engine frontend (from request arrival at frontend to arrival at the application) as a function of version count. This requires trace-level visibility into the frontend layer.
Result: The frontend routing time increased by several hundred milliseconds at p99 for instances with many versions. The routing layer was computing version-to-instance mappings on every request for versions that had been deployed for a long time, rather than caching the mapping.
Conclusion: Hypothesis supported. Root cause identified.
Resolution
- Remove old, unused versions from App Engine instances.
- Implement a caching layer in the frontend routing logic.
- Apply a limit on the number of concurrently deployed versions.
Key Takeaways from the Case Study
- Multiple hypotheses were tested and refuted. Each negative result was valuable because it narrowed the search space and eliminated plausible alternative explanations.
- Observational experiments were used first. The team did not start by blindly restarting services or migrating instances; they measured.
- The correlation-versus-causation trap was avoided. The correlation between version count and latency was noted, but the team did not stop there — they tested specific causal mechanisms.
- The tail (p99) was more informative than the median. The failure mode was most visible at the tail, which pointed toward a sporadic overhead rather than a constant one.
- The root cause was an emergent property. The latency was not caused by a bug in the application code or a single misconfiguration, but by an emergent effect of many small factors combining — exactly the kind of failure that systematic troubleshooting excels at catching.
Building for Observability
The best troubleshooting is the troubleshooting that never needs to happen — because the system makes its internal state visible. Observability is the property of a system that allows you to understand its behavior from the outside, without having to deploy new code or restart it.
White-Box Metrics
Black-box metrics (is the service up? is it responding?) tell you that something is wrong. White-box metrics tell you what it is.
Examples of white-box metrics every service should expose:
- Request counts, error counts, and latency distributions (histograms) by: endpoint, status code, caller, and error type.
- Queue depths: request queue, work queue, message queue.
- Connection pool state: active, idle, waiting, max size.
- Cache hit/miss ratios per cache.
- Rate limiter state: tokens available, requests throttled.
- Resource pool sizes and utilization (threads, goroutines, file descriptors).
- GC or allocation metrics: pause time, allocation rate, heap size.
Structured Logging
Unstructured log lines are nearly impossible to query at scale. Every log entry should be a structured event with a consistent schema.
Minimum required fields:
timestamp— RFC 3339 with microsecond precision.severity— DEBUG, INFO, WARN, ERROR, FATAL.service— Name of the service producing the log.trace_id— The distributed trace ID for correlation across services.request_id— A unique identifier for the request (may match trace_id).message— Human-readable description.error— If an error: error type, error message, stack trace (first N frames).
Additional useful fields:
user_idorcustomer_idfor user-specific debugging.instance_idorpod_namefor instance-level debugging.version— Application version or commit SHA.latency_ms— Time spent in the current operation.http_method,http_path,http_status— For HTTP services.
Consistent Request IDs
A single request may traverse dozens of services. Without a consistent request ID propagated through all of them, reconstructing the request's path is impossible.
Best practices:
- Generate a unique request ID at the first entry point (load balancer, API gateway, or ingress).
- Propagate it via HTTP headers (
X-Request-Id,traceparent) to all downstream services. - Include it in every log entry, every trace span, and every error report.
- Return it to the client in the response headers so that users can reference it in support tickets.
- Use W3C Trace Context (
traceparent/tracestate) for compatibility with open-source tracing systems.
The Three Pillars of Observability
- Metrics — Aggregated, time-series data about the system. Best for alerting and dashboards. Low cardinality.
- Logs — Discrete events with structured payloads. Best for detailed investigation of specific requests or errors. High cardinality.
- Traces — End-to-end views of a single request across service boundaries. Best for understanding latency bottlenecks and service dependencies.
All three pillars are necessary. Metrics tell you "something is broken." Logs tell you "here is what went wrong for this request." Traces tell you "here is exactly where it went wrong in the request path."
Instrumentation as Investment
Observability is an investment with compound returns. Every metric, log field, and trace span you add today will pay dividends in every future incident. The marginal cost of adding one more counter or one more log field is near zero; the marginal benefit during a P0 incident is enormous.
A practical rule: instrument every dependency call. Every RPC, every database query, every cache lookup, every queue enqueue/dequeue should produce at minimum:
- A latency metric (histogram or summary).
- A success/failure counter.
- A log line on error with the error details.
- A trace span with the dependency name and timing.
Troubleshooting Expertise: Systematic vs Experience-Based
Not all troubleshooting expertise is the same. Understanding the two major types — systematic and experience-based — helps you know when to rely on each and how to cultivate both.
Systematic Troubleshooting
Systematic troubleshooters apply the hypothetico-deductive method rigorously. They follow a process regardless of the domain:
- They generate multiple hypotheses before testing any.
- They design experiments with controls and confounding factors in mind.
- They document each hypothesis and result.
- They can work effectively on any system, even ones they have never seen before.
- They are slower on familiar problems but more reliable on unfamiliar ones.
Strengths:
- Works for novel or unprecedented failures.
- Produces reproducible, documented results.
- Teaches well and scales across teams.
- Resistant to cognitive biases.
Weaknesses:
- Slower for routine or well-known problems.
- Can feel mechanical or overly academic.
- Requires discipline to maintain when under time pressure.
Experience-Based Troubleshooting
Experienced troubleshooters rely on pattern matching and intuition built from years of solving similar problems:
- "This looks like the DNS propagation issue we had last month."
- "I've seen this exact error before — it's the connection pool leak."
- "That latency pattern is classic noisy neighbor on the hypervisor."
Strengths:
- Extremely fast for familiar failure modes.
- Intuition can sometimes leap past intermediate steps.
- Pattern recognition improves with every incident.
Weaknesses:
- Brittle when faced with novel failures.
- Prone to confirmation bias (seeing the same pattern everywhere).
- Hard to transfer or teach ("it's just a feeling").
- Can lead to premature closure.
The Hybrid Approach
The best SREs combine both approaches:
- Start with pattern recognition. "This looks like X — let me check these three specific metrics quickly." This is fast and often correct.
- If pattern matching fails (within 5-10 minutes), switch to systematic. "The familiar patterns don't fit. Let me step back, generate hypotheses, and design experiments."
- Document both. When you find the root cause, note whether it matched a known pattern or was novel. Feed novel findings into your team's knowledge base.
Building Experience Systematically
Systematic troubleshooting is the foundation because it can be taught, practiced, and improved. Experience is then built on top of it:
- Every incident adds a new pattern to your mental library.
- Every postmortem connects the pattern to the root cause mechanism.
- Every runbook turns a pattern into a playbook for the next occurrence.
- Every blameless culture encourages sharing both successes and failures.
In short: systematic method is the engine; experience is the fuel. You need both, but the engine must come first because without it, experience is just a collection of anecdotes with no way to distinguish correlation from causation.
References and Further Reading
- Beyer, B., Jones, C., Petoff, J., & Murphy, N. R. (2016). Site Reliability Engineering: How Google Runs Production Systems. O'Reilly Media.
- Beyer, B., Murphy, N. R., Rensin, D., Kawahara, K., & Thorne, S. (2018). The Site Reliability Workbook: Practical Ways to Implement SRE. O'Reilly Media.
- Adkins, H., Beyer, B., & Thurnherr, V. (2020). Building Secure and Reliable Systems. O'Reilly Media.
- Driscoll, M. (2016). Debugging: The 9 Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems. AMACOM.
- Agans, D. J. (2006). Debugging: The 9 Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems. AMACOM.
- Petoff, J. (2015). "Troubleshooting Distributed Systems: The Hypothethico-Deductive Method." USENIX SREcon.
- Tufte, E. R. (1997). Visual Explanations: Images and Quantities, Evidence and Narrative. Graphics Press.
- Kahneman, D. (2011). Thinking, Fast and Slow. Farrar, Straus and Giroux.
- Kleppmann, M. (2017). Designing Data-Intensive Applications. O'Reilly Media.
- Google SRE Team. "The 'What To Do When You Break Production' Guide." Internal Google SRE Documentation.
- Google SRE Team. "App Engine Latency Case Study." In Site Reliability Engineering, Chapter 14.