How To

Debugging Production Incidents: A Systematic Framework

A fixed sequence for debugging production issues while the pager is still going: stabilize, check what changed, form hypotheses with USE and RED, bisect the request path by layer, then roll back or fix forward with a rubric. Every step has a working command, and one incident is walked through the whole framework.

14 min read
Cover Image for Debugging Production Incidents: A Systematic Framework

Debugging Production Incidents: A Systematic Framework

It's 2:47 a.m. Checkout latency is climbing, error rates are spiking in one region, and three people are typing over each other in the incident channel. Nobody knows if this started five minutes ago or fifty. This is where most debugging of production issues actually happens, and it's exactly where ad hoc debugging fails.

Debugging production issues is different from debugging in development. You can't attach a debugger, you can't reproduce at will, and every minute of diagnosis has a customer-facing cost. What you need instead is a framework: a fixed sequence of decisions that works when you're tired, stressed and looking at a system someone else built. This article gives you that framework: stabilize first, check what changed, measure with USE and RED, bisect by layer, then decide between rollback and fix-forward.

This is a companion piece to our broader root cause analysis guide. That article covers the full RCA lifecycle including the postmortem. This one covers the part where the pager is still going off.

01First

Stabilize

Reduce customer impact with coarse, reversible levers.

02Cheapest check

What changed

Deploys, config, flags, data and scheduled work near onset.

03Hypotheses

USE and RED

Saturated resource or sick service: metrics pick the direction.

04Search

Bisect layers

Test a midpoint and cut the search space in half.

05Decide

Roll back or fix

A rubric, not a debate, with the timeline as evidence.

The framework in order. Keep a timestamped timeline running the whole way through.

Why ad hoc debugging breaks at 3 a.m.

Smart engineers still lose hours in incidents. The reasons are structural, not personal:

Diagnosis feels like progress. Reading logs and forming theories is satisfying work, so responders do it first, while customers keep hitting errors that a rollback could have stopped ten minutes ago.

Logs are searched before there's a hypothesis. Grepping a noisy service for "error" returns thousands of lines, most of them normal. Without a question to answer, there's no way to tell which line matters.

Memory rewrites the timeline. Under stress, "the deploy went out, then latency spiked" and "latency spiked, then we deployed the fix" blur together. Decisions made on a misremembered order go wrong.

The system model lives in a few heads. Which service calls which, and where the connection pool limits are, is knowledge the senior engineer has and the 3 a.m. responder may not.

A framework doesn't make anyone smarter. It removes these four failure modes by fixing the order of operations.


First principles: stabilize before you understand

The single most common mistake in production incident debugging is treating diagnosis as the first job. It isn't. Your first job is to reduce customer impact. Understanding the root cause is the second job, and it can usually wait.

Split every incident into two explicit tracks:

  • Mitigate. Restore service using coarse, reversible actions: roll back the last deploy, shift traffic away from a bad zone, scale out, enable a degraded mode, shed noncritical load. These actions don't require understanding the root cause. They require knowing which lever is safe to pull.
  • Diagnose. Figure out what actually broke. This track can run in parallel, at lower urgency, and often continues after the customer impact is gone.

If you have more than one responder, assign the tracks to different people. If you're alone, do the mitigation pass first and time-box it: spend the first ten minutes asking "what can I do right now to make this stop hurting" rather than "why is this happening." Teams that adopt a mitigate-first posture typically cut incident duration significantly (often by 30 to 50 percent on change-induced incidents) simply because rollback is faster than diagnosis. If your mean time to recovery is dominated by diagnosis time, that's a process smell; see our guide on how to reduce MTTR for the structural fixes.

The second first-principle habit: write the timeline as you go. Open a scratch doc or a thread and log timestamps for everything: when the alert fired, when symptoms started (not the same thing), what you checked, what you changed, what happened after. Two reasons. It stops you from re-checking the same dashboard three times at 3 a.m. And the timeline is the raw material for the postmortem; a timeline reconstructed from memory two days later is fiction. One line per event is enough:

02:41 p99 checkout latency crosses 2s (alert fired 02:47)
02:52 rolled back checkout to rev 141, no effect
02:58 noticed payments-db CPU pinned since 02:39
03:04 killed long-running analytics query, latency recovering

Note the ordering trap this example exposes: the rollback did nothing because the deploy wasn't the cause. You only see that clearly because the timestamps are written down.


The change-first heuristic

Before any deep diagnosis, do one cheap thing: check what changed. The majority of production incidents follow a change; industry postmortem datasets and vendor incident analyses consistently attribute somewhere between half and three-quarters of incidents to a recent change. "Change" means more than deploys:

  • Code deploys. The obvious one.
  • Config changes. Often shipped through a separate, less-scrutinized pipeline.
  • Feature flags. A flag flip is a deploy that skips CI.
  • Data shape. A new customer with 100x the usual cardinality, a batch import, a row that breaks an assumption.
  • Dependency behavior. A third-party API changing latency, rate limits or response format without telling you.
  • Scheduled work. Certificate rotations, cron jobs, autoscaling events, infrastructure maintenance.

The check itself takes under a minute:

kubectl rollout history deployment/checkout -n production
git log --oneline --since="6 hours ago" -- deploy/ config/

If a change lands within a few minutes of symptom onset, treat it as the prime suspect until proven otherwise. Correlation isn't causation, but at 3 a.m. it's an excellent place to start, and reverting a correlated change is cheap to test.

The heuristic only works if changes are visible. If deploys, flag flips and config pushes don't appear on the same timeline as your metrics, you won't make the connection under pressure. The fix is deploy annotations: have your CD pipeline post an event to your metrics system on every change:

curl -s -X POST "$GRAFANA_URL/api/annotations" \
  -H "Authorization: Bearer $GRAFANA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tags":["deploy","checkout"],"text":"checkout v142 deployed"}'

Once every dashboard shows vertical lines for changes, "did this start after the deploy?" stops being a research task and becomes something you answer by looking.


Measure before you guess: USE and RED

If no change explains the symptoms, or mitigation is underway and you need a direction, resist the urge to SSH into a random box and start reading logs. Logs are where you confirm hypotheses, not where you generate them. Generate hypotheses from metrics, using two complementary checklists.

01USE, per resource

Ran out of something

Utilization, saturation and errors on CPU, memory, disk, network and pools.

02RED, per service

Something misbehaves

Rate, errors and duration (p99, not the mean) for each service.

Sick service on healthy resources points at code or a dependency. A saturated resource points at capacity or a workload change.

The USE method: per resource

Brendan Gregg's USE method says: for every resource (CPU, memory, disk, network, connection pools, thread pools), check Utilization, Saturation and Errors. It catches the class of incidents where nothing changed in your code but the system ran out of something: capacity exhaustion, noisy neighbors, leaks that finally crossed a threshold.

Utilization tells you how busy the resource is. Saturation tells you how much work is queued because the resource can't keep up, and saturation, not utilization, is what correlates with latency. A node at 90 percent CPU with an empty run queue is fine. A node at 70 percent with a deep run queue isn't.

A practical PromQL check for CPU saturation across nodes, using pressure stall information:

rate(node_pressure_cpu_waiting_seconds_total[5m]) > 0.1

This fires when processes are collectively spending more than 10 percent of their time waiting for CPU, which is to say queueing. If you don't export pressure metrics, comparing load average to core count on the box works as a fallback:

uptime && nproc

Walk the USE checklist across every resource in the suspect path. It takes five minutes, and it's exhaustive in a way intuition isn't.

The RED method: per service

The RED method is the service-level complement: for every service, check Rate (requests per second), Errors (failed requests per second) and Duration (latency distribution, not averages). It catches the incidents USE misses: a downstream dependency returning errors fast, a bad code path that's slow without consuming resources, a traffic spike from one client.

A p99 duration check for a service instrumented with Prometheus histograms:

histogram_quantile(0.99,
  sum by (le) (
    rate(http_request_duration_seconds_bucket{job="checkout"}[5m])
  )
)

Look at p99, not the mean. Production incidents almost always live in the tail first. By the time the mean moves, your worst-affected users have been suffering for a while.

Use the two methods together as a decision fork. If RED shows a sick service but USE shows healthy resources underneath it, the problem is in the code or in a dependency. If USE shows a saturated resource, the problem is capacity or a workload change. Either way, you now have a direction instead of a guess.


Bisect by layer

Distributed systems debugging is a search problem, and the fastest search is binary. A request in a typical mid-market SaaS stack crosses six layers:

client → edge/CDN/LB → service → dependency services → data store → infrastructure

01Layer 1

Client

curl timing splits DNS, connect, TLS and first byte.

02Layer 2

Edge and LB

Are the targets behind the load balancer healthy?

03Layer 3

Service

Orchestrator events: OOM kills, crash loops, failed probes.

04Layer 4

Dependencies

Test from inside the caller, not from your laptop.

05Layer 5

Data store

Long queries, lock waits and connection pile-ups.

06Layer 6

Infrastructure

Node conditions before blaming anything above them.

Six layers in a typical request path. Start at a midpoint, not at the top.

Don't walk this list top to bottom. Pick a midpoint, test it, and cut the search space in half. If the service looks healthy from its own metrics but clients see errors, the problem is above the service: edge, LB, DNS, TLS. If the service itself is reporting errors, the problem is at the service or below. Each test should eliminate roughly half of the remaining layers.

Concrete probes for each cut:

Client to edge. Reproduce the request with a timing breakdown to see which phase is slow (DNS, TCP connect, TLS or server processing):

curl -o /dev/null -s -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' https://api.example.com/healthz

If time to first byte is fine but total is slow, the response body transfer is the problem. If TLS handshake time jumped, look at the edge, not the service.

Edge to service. Check whether the load balancer even considers your backends healthy:

aws elbv2 describe-target-health \
  --target-group-arn "$TG_ARN" \
  --query 'TargetHealthDescriptions[?TargetHealth.State!=`healthy`]'

A surprising number of "the service is down" incidents are actually "the health check is failing, so the LB removed every target." The service processes are fine.

Service layer. Ask the orchestrator what's been happening, in time order:

kubectl get events --sort-by=.lastTimestamp -n production | tail -20

OOM kills, failed scheduling, crash loops and failed probes all show up here before they show up anywhere else.

Service to dependency. Test the dependency from inside the caller's network context, because your laptop's view of the dependency proves nothing about the pod's view:

kubectl exec deploy/checkout -n production -- \
  curl -s -o /dev/null -w '%{http_code} %{time_total}\n' http://payments:8080/healthz

Data store. Look at what the database is actually doing right now (long-running queries, lock waits, connection pile-ups):

SELECT pid, now() - query_start AS duration, wait_event_type, state, left(query, 80)
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY duration DESC
LIMIT 10;

Infrastructure. Check node conditions before blaming anything above them:

kubectl describe nodes | grep -A5 Conditions

The prerequisite for effective bisection is knowing what the layers are: which services sit in the request path and what they depend on. If your team can't draw that diagram from memory during an incident, that's a gap worth closing before the next one. We cover how in dependency mapping for root cause analysis.


One incident, through the framework

Here's the timeline from earlier, walked step by step.

Stabilize and check changes. The p99 alert fires at 02:47. The rollout history shows checkout rev 142 went out the previous evening, the only recent change on the path. It's the cheapest suspect, so it gets rolled back at 02:52. Latency doesn't move. That's useful: one hypothesis is dead, and the timeline says so.

Name the remaining readings. Without a change to blame, there are a few possibilities. Checkout's own code path is slow under some new input. A dependency (payments, or its database) is slow. Or a shared resource is saturated. RED on checkout shows rate flat, errors up slightly and p99 way up: it's sick, but not overloaded. USE on checkout's pods shows healthy CPU and memory. Sick service on healthy resources points at a dependency.

Bisect downward. The kubectl exec probe from a checkout pod to payments:8080/healthz takes seconds instead of milliseconds. The problem is at payments or below. Payments' pods look healthy too, so the next cut is the data store. pg_stat_activity on payments-db shows an analytics query that's been running since 02:39, holding locks, with a queue of payment queries waiting behind it. The CPU graph confirms it: pinned since 02:39, two minutes before the first latency symptom.

Decide. There's nothing to roll back, since the cause wasn't a deploy. The mitigation is to kill the query (03:04), and the fix-forward is to move analytics to a replica. The postmortem writes itself from the timeline, including the rollback that didn't help.


Roll back or fix forward

You found a suspect change. Now the decision that actually determines incident duration: revert it, or fix it in place. Use a rubric, not a debate.

SignalRoll backFix forward
Change is recent and correlated with onsetYesNo
Rollback path is tested and takes minutesYesNo
Change included a schema migration or data backfillNoUsually yes
Other changes have shipped on top of the suspectDepends on isolationOften yes
Cause is external (provider outage, upstream API)Nothing to roll backMitigate around it
Fix is a one-line, well-understood patch with fast CINoMaybe
It's 3 a.m. and you're not sureYesNo

Default to rollback when the change is recent, suspect and cheap to revert. Rollback is a mitigation you can execute in one command without understanding the bug:

kubectl rollout undo deployment/checkout -n production

Fix forward when rollback is genuinely riskier than the bug: a migration already rewrote data in a shape the old code can't read; the suspect change is entangled with later changes you'd also revert; or the cause is external and there's nothing of yours to revert. Be honest about the cost. Fixing forward means writing code under pressure, reviewing it under pressure, and shipping it through a pipeline that takes however long it takes, all while the incident continues.

Two failure modes to avoid. Ego-driven fix-forward: "I can patch this in five minutes" is how five-minute incidents become ninety-minute incidents. And rollback theater: rolling back a change the timestamps already exonerated, which is another reason the written timeline matters.

One structural note: if rollback is scary on your team, that's the actual incident. A rollback path you haven't exercised recently isn't a rollback path; it's a hope. Test it in daylight.


What frameworks can and can't do

A framework reduces panic. It gives you a next step when your mind is blank, keeps three responders from duplicating work, and stops you from spending forty minutes reading logs for a hypothesis you never formed. Those are real gains.

What it doesn't do is replace system knowledge. USE and RED tell you where to look. Knowing that the checkout service holds a connection pool of exactly 20 and falls over at 21 is knowledge no checklist contains. The engineers who resolve incidents fastest carry an accurate mental model of the system, and the framework keeps that model pointed in a productive direction under stress.

The framework's most durable output isn't the fix. It's the timeline: a defensible, timestamped record of what was observed, what was tried and what worked. That record turns an incident into a postmortem worth reading, and a postmortem worth reading is what stops the same incident from happening twice.


Why the framework beats ad hoc debugging

Ad hoc debuggingFramework by handFramework run by CloudThinker
First ten minutesReading logs for a theoryMitigate, then check changesChange correlation done before you open a laptop
HypothesesWhatever the loudest person suspectsFrom USE and REDFrom USE and RED across every service in the path
SearchTop to bottom, or randomBisect by layerBisect across a live dependency graph
TimelineRebuilt from memoryWritten as you goReconstructed from telemetry automatically
System knowledgeIn a few headsIn a few headsIn the topology graph, shared by everyone

The deeper difference is where the working memory lives. A framework run by a person depends on that person's stamina and knowledge of the system at 3 a.m. The same framework run against telemetry and a live topology doesn't get tired, and it doesn't forget which services sit in the path.


Where CloudThinker fits

Everything above is executable by a human with a terminal. It's also what CloudThinker Resolve runs when an alert fires: reconstructing the timeline from your telemetry, correlating symptom onset against deploys, flag flips and config changes, and bisecting layer by layer across a live topology graph of your services and dependencies. You get the completed investigation instead of running it yourself at 3 a.m.

Mitigation is where an agent has to be careful, so it goes through Auto Mode, so you decide what runs on its own and what waits for approval. The investigation (metrics, logs, events, queries like the ones above) runs freely. Every write, whether a rollback, a scale-out or killing a query, is proposed with its evidence. Every proposal and action lands in the audit trail alongside the timeline.


Getting started

Ask in plain language, the same way you'd brief a teammate joining the incident:

"Checkout p99 has been climbing since about 02:40. Check what changed in the last six hours, run USE on the pods and nodes in the checkout path and RED on checkout and its dependencies, and tell me which layer the problem is in. Propose only, change nothing."

"Reconstruct the timeline for last night's checkout incident from alerts, deploys and the actions taken in the channel, and draft a postmortem from it."


Related reading

Try it free at app.cloudthinker.io. The free tier includes 100 premium credits, no card required.

incidentresponsercasreagenticopsdebugging