Root Cause Analysis in Production

By Oleksandr Andrushchenko — Published on
0 Likes
0 Dislikes
Root Cause Analysis in Production
Root Cause Analysis in Production

Production incidents rarely have one obvious cause. A latency spike may begin with a slow dependency, trigger retries, exhaust connection pools, increase queue depth, and eventually cause unrelated services to fail. By the time the incident is visible to users, several components may be unhealthy at once.

Root cause analysis is the process of reducing that complexity into a causal explanation: what changed, where the failure started, how it propagated, and why existing protections did not contain it. The goal is not to find one person or one bad line of code. The goal is to understand the technical chain of events well enough to prevent recurrence.

Effective RCA depends on correlated telemetry, deployment history, dependency awareness, timelines, and disciplined hypothesis testing. The fastest investigations move from symptoms to evidence, then from evidence to causal relationships, while avoiding guesses based on the first suspicious metric or error message.

Table of Contents

Symptom vs Root Cause

The first visible failure is often not the root cause.

Suppose checkout latency rises from 300 ms to 4 seconds:

Visible symptom:

checkout p99 = 4 sec

That could be caused by:

  • database connection saturation;
  • slow external payment provider;
  • cache failure increasing database traffic;
  • retry amplification;
  • CPU saturation;
  • cross-region network latency;
  • a deployment introducing an extra dependency call.

An RCA should separate symptoms, contributing factors, and root causes.

Root cause
    |
    v
Dependency slowdown
    |
    v
Retries increase
    |
    v
Connection pool saturates
    |
    v
Request latency rises
    |
    v
User-visible errors

In this chain, high latency and errors are symptoms. Connection pool saturation may be a contributing factor. The original dependency slowdown may be the initiating root cause.

But even that explanation may be incomplete. If retries had no bounds, the retry policy may also be a root cause of the incident severity.

A useful RCA therefore asks two different questions:


What initiated the incident?

What allowed the incident to become severe?

The second question often reveals missing circuit breakers, absent backpressure, insufficient headroom, unsafe deployment behavior, or poor retry design.

Building an Incident Timeline

A reliable RCA starts with a timeline. Without one, engineers tend to reason from memory and correlate events that may not actually be related.

A timeline should include:

  • first abnormal metric;
  • first user-visible impact;
  • deployments and configuration changes;
  • dependency degradation;
  • alert timestamps;
  • mitigation actions;
  • recovery timestamps.

For example:

14:02  payment-service v82 deployed to 10%
14:05  DB pool wait time begins increasing
14:07  payment p99 exceeds 1 sec
14:08  checkout error rate rises above 2%
14:09  alert fires
14:12  rollout reaches 50%
14:14  DB pool fully saturated
14:15  checkout error rate reaches 11%
14:17  deployment rollback starts
14:20  new v82 traffic stops
14:24  DB pool waiters return to normal
14:27  checkout error rate recovers

This sequence strongly suggests the deployment is relevant, but correlation alone is not enough. The next step is proving what changed in v82 and how that change caused pool saturation.

Timelines should include system state, not only human actions:

Deployment
   |
   v
new version receives traffic
   |
   v
extra DB query per request
   |
   v
connection occupancy increases
   |
   v
pool waiters increase
   |
   v
latency increases

That turns chronology into causality.

Using Logs, Metrics, and Traces Together

Strong RCA usually requires more than one telemetry signal. Metrics establish scope, traces show request paths, and logs provide local context.

Suppose an alert reports:

checkout error rate = 8.4%
checkout p99 = 3.1 sec

Metrics answer:


When did the problem start?

Which region?

Which version?

What percentage of traffic is affected?

Which resource became saturated?

A dashboard may show:

us-east:
p99 = 3.1 sec
errors = 8.4%

us-west:
p99 = 320 ms
errors = 0.4%

The incident is regional.

Traces narrow the path:

POST /checkout                3.0 sec
 |
 +-- order-service            2.9 sec
      |
      +-- payment-service     2.7 sec
           |
           +-- DB pool wait   2.2 sec
           |
           +-- query           60 ms

The bottleneck is not query execution. It is waiting for a database connection.

Logs then add detail:

{
  "service": "payment-service",
  "version": "v82",
  "region": "us-east",
  "event": "db_connection_wait",
  "trace_id": "abc123",
  "pool_active": 100,
  "pool_max": 100,
  "pool_waiters": 1842,
  "wait_ms": 2204
}

Additional metrics show:

db_pool_active = 100
db_pool_max = 100
db_pool_waiters = 1,842

The investigation path becomes:

Metrics
"What changed and where?"
    |
    v
Traces
"Which operation dominates?"
    |
    v
Logs
"What exactly happened there?"
    |
    v
Metrics
"How widespread is it?"

For the telemetry architecture behind this workflow, see Observability Explained: Logs, Metrics, and Traces.

Hypothesis-Driven Investigation

RCA becomes inefficient when engineers browse dashboards without a question. A better approach is to create hypotheses and try to disprove them quickly.

Suppose API latency rises after a release.

Possible hypotheses:

H1: new version increases DB load
H2: external provider is slow
H3: cache hit rate dropped
H4: CPU saturation is causing latency
H5: cross-region traffic increased

Each hypothesis should have expected evidence.

Hypothesis Expected Evidence Disproving Evidence
Database pressure Pool waits, query volume, DB latency rise DB metrics unchanged
Provider slowdown Provider spans become slower Provider latency unchanged
Cache regression Hit ratio falls and DB reads rise Cache hit ratio stable
CPU saturation Run queue and CPU rise with latency CPU remains low and waits happen elsewhere
Cross-region traffic More remote calls and network RTT Traffic topology unchanged

This method prevents one suspicious graph from becoming a premature conclusion.

Useful RCA questions include:

  • What changed immediately before the incident?
  • Did the problem affect all traffic or one segment?
  • Which component first became abnormal?
  • What resource started waiting?
  • Did retries increase request volume?
  • Did failure handling work as intended?
  • What evidence would disprove the current hypothesis?

A hypothesis should be updated when evidence changes rather than defended because it was proposed first.

Common Root Cause Patterns

Many incidents look unique at the product level but follow recurring distributed-systems patterns underneath. Recognizing those patterns speeds investigation.

Capacity and Saturation

Capacity incidents often start before CPU or memory reaches 100%.

The real constrained resource may be:


DB connections
worker slots
thread pools
HTTP client pools
queue consumers
rate-limit quota
network bandwidth
file descriptors
disk IOPS

A typical pattern is:


Traffic increases
      |
      v
Resource utilization rises
      |
      v
Waiting work appears
      |
      v
Latency rises
      |
      v
Timeouts begin
      |
      v
Retries increase
      |
      v
Load increases further

The strongest early indicator is often waiting work:

db_pool_waiters
queue_oldest_message_age
thread_pool_queue
request_queue_time

Saturation metrics should therefore be designed around where work waits, not just around resource percentages.

Dependency and Retry Failures

External dependencies can degrade without failing completely. Slow responses are often more dangerous than immediate failures because they occupy resources longer.


Provider latency:
100 ms --> 5 sec

Worker concurrency:
100

Maximum throughput before:
~1000 operations/sec

Maximum throughput during slowdown:
~20 operations/sec

Even with unchanged incoming traffic, throughput collapses.

Retries can amplify the problem:


Original request
    |
    +--> attempt 1 timeout
    |
    +--> attempt 2 timeout
    |
    +--> attempt 3 timeout

If 10,000 original requests per second each generate three attempts:


10,000 original requests/sec
        |
        v
up to 30,000 downstream attempts/sec

This is why retry metrics and traces are essential during dependency incidents.

For deeper retry behavior, see Timeouts, Retries, and Exponential Backoff.

Deployment and Configuration Regressions

Deployments are frequent root causes because they intentionally change system behavior.

Useful deployment dimensions include:


service.version
deployment_id
feature_flag
region
canary_group

Suppose:


v81:
p99 = 280 ms
errors = 0.4%

v82:
p99 = 1.9 sec
errors = 6.7%

This immediately narrows the investigation.

However, the RCA should still determine the actual mechanism:


v82
 |
 +--> new eager-loading behavior
 |
 +--> 2 extra queries/request
 |
 +--> DB connection occupancy rises
 |
 +--> pool waiters increase
 |
 +--> latency and errors rise

The root cause is not merely "bad deployment." The useful explanation is what behavior changed and why the safeguards failed to contain it.

Understanding Failure Propagation

A mature RCA should explain not only the initiating fault but also how it propagated through the system.

Consider a carrier integration incident:


Carrier API latency
100 ms --> 8 sec
        |
        v
Booking workers blocked
        |
        v
Worker throughput falls
        |
        v
Booking queue grows
        |
        v
Oldest message age increases
        |
        v
Users see delayed bookings

Now add retries:


Carrier slow
   |
   v
Timeout
   |
   v
Retry
   |
   v
More blocked workers
   |
   v
Lower effective throughput

Now add an unbounded queue:


Incoming > processing
      |
      v
Backlog accumulates
      |
      v
Recovery takes hours
even after carrier recovers

The initiating failure is the carrier slowdown. But the retry policy and insufficient isolation explain why the incident became severe.

A useful post-incident causal model is:


Trigger
  |
  v
Primary failure
  |
  v
Amplification mechanism
  |
  v
Resource saturation
  |
  v
User impact
  |
  v
Recovery behavior

This structure leads directly to preventive actions.

Production Design Example

Consider a logistics platform where customers create shipments and the system asynchronously books them with external carriers.


                           Clients
                              |
                              v
                         API Gateway
                              |
                              v
                      Shipment Service
                              |
                    +---------+---------+
                    |                   |
                    v                   v
               PostgreSQL         Booking Queue
                                      |
                                      v
                                Booking Workers
                                      |
                             +--------+--------+
                             |                 |
                             v                 v
                        Carrier A         Carrier B

At 09:00, users begin reporting delayed bookings.

Step 1: Confirm user impact.


booking completion p95:
45 sec --> 11 min

booking success rate:
99.4% --> 71%

Step 2: Scope the problem.


Carrier A:
error rate = 36%

Carrier B:
error rate = 0.7%

The incident is isolated to Carrier A traffic.

Step 3: Inspect queue behavior.


booking queue depth:
40K --> 2.1M

oldest message age:
8 sec --> 14 min

The queue is accumulating faster than workers can process it.

Step 4: Inspect worker traces.


booking.process                17 sec
 |
 +-- carrier-a attempt 1        5 sec timeout
 |
 +-- retry backoff              1 sec
 |
 +-- carrier-a attempt 2        5 sec timeout
 |
 +-- retry backoff              2 sec
 |
 +-- carrier-a attempt 3        4 sec timeout

Each failed booking occupies one worker for approximately 17 seconds.

Before the incident:


carrier call ~100 ms
worker throughput high

During the incident:


carrier call ~5 sec timeout
multiple retries
worker throughput collapses

Step 5: Inspect capacity.


Workers:
500

Normal processing:
~10 bookings/sec/worker

Potential normal capacity:
~5,000 bookings/sec

During timeout storm:
~0.06 failed bookings/sec/worker

Effective failed-flow capacity:
~30 bookings/sec

The dependency slowdown reduced effective worker throughput by orders of magnitude.

Step 6: Identify amplification.


Carrier slowdown
      |
      v
5-second timeout
      |
      v
3 retry attempts
      |
      v
workers blocked ~17 sec
      |
      v
queue grows rapidly

Step 7: Explain why safeguards failed.

The system had retries, but:

  • timeouts were too long for the workload;
  • retries happened inside the same worker slot;
  • Carrier A had no separate concurrency limit;
  • bulkhead isolation between carriers was missing;
  • queue growth had no early page based on oldest-message age.

The root cause statement can now be specific:


Carrier A latency increased sharply.

Long per-attempt timeouts and three inline retries
caused Carrier A bookings to occupy worker slots for
approximately 17 seconds each.

Because carrier workloads shared the same worker pool,
effective booking throughput collapsed and the booking
queue accumulated millions of messages.

Missing per-carrier bulkhead limits allowed one
dependency to reduce capacity for the entire pipeline.

This explanation is much more useful than "Carrier A was down."

Monitoring. Future monitoring should include per-carrier latency, retry rate, timeout rate, queue age, worker occupancy, and concurrency by carrier.

Scaling. Adding workers can temporarily increase throughput, but it can also increase pressure on the already failing provider. Scaling should be combined with concurrency limits and backpressure.

Deployment considerations. Retry, timeout, and concurrency-policy changes should be rolled out gradually because they directly affect downstream load.

Ready-to-Use Example

A practical RCA document should capture evidence, not only conclusions. A simple structured format can keep incident analysis consistent.

incident:
  title: "Carrier A booking backlog"
  started_at: "2026-08-23T09:00:00Z"
  resolved_at: "2026-08-23T09:42:00Z"

impact:
  booking_p95_before: "45s"
  booking_p95_during: "11m"
  booking_success_before: "99.4%"
  booking_success_during: "71%"

trigger:
  component: "Carrier A API"
  condition: "request latency increased from ~100ms to 5s timeouts"

amplification:
  - "three inline retries per booking"
  - "shared worker pool across carriers"
  - "no per-carrier concurrency limit"

root_causes:
  - "carrier dependency slowdown"
  - "retry and isolation policy allowed one dependency to consume shared worker capacity"

mitigation:
  - "disabled Carrier A retries"
  - "reduced timeout"
  - "temporarily routed eligible traffic to Carrier B"

follow_up:
  - "add per-carrier concurrency bulkheads"
  - "alert on booking queue oldest-message age"
  - "add retry-rate dashboard by carrier"
  - "load-test dependency timeout scenarios"

Useful RCA data can also be queried directly from application databases when workflow state matters.

For example, identifying delayed shipment bookings:

SELECT
    carrier,
    COUNT(*) AS delayed_bookings,
    MAX(EXTRACT(EPOCH FROM (NOW() - created_at))) AS oldest_age_seconds
FROM shipment_bookings
WHERE status = 'pending'
  AND created_at < NOW() - INTERVAL '5 minutes'
GROUP BY carrier
ORDER BY oldest_age_seconds DESC;

The query provides business-state evidence that complements telemetry.

A deployment comparison query can reveal whether errors are version-specific:

SELECT
    service_version,
    COUNT(*) FILTER (
        WHERE status = 'failed'
    ) AS failed,
    COUNT(*) AS total
FROM booking_attempts
WHERE created_at > NOW() - INTERVAL '30 minutes'
GROUP BY service_version
ORDER BY service_version;

Production RCA often requires combining observability data with durable business state because telemetry may be sampled or incomplete.

Common Mistakes

Mistake Production Impact Better Approach
Calling the first visible error the root cause Corrective actions target symptoms instead of the initiating failure. Build a causal chain from trigger to impact.
Assuming correlation proves causation Deployments or metrics may be blamed without evidence. Verify the mechanism connecting the events.
Investigating without a timeline Events are interpreted in the wrong order. Build a timestamped incident sequence first.
Browsing dashboards without hypotheses Investigation becomes slow and unfocused. Form testable hypotheses and look for expected evidence.
Ignoring healthy comparison traffic Abnormal behavior is harder to identify. Compare failing regions, versions, and requests with healthy ones.
Focusing only on CPU Connection pools, queues, and external dependencies are missed. Inspect where work is waiting.
Ignoring retries Traffic amplification and extended latency remain unexplained. Measure attempts, retries, and backoff explicitly.
Ignoring queue age Backlog severity is underestimated. Track oldest-message age and processing throughput.
Stopping at the external dependency failure Internal amplification mechanisms remain unfixed. Explain why the dependency failure propagated.
Writing "human error" as root cause The technical control weakness remains unresolved. Identify why the system allowed the action to create impact.
Using sampled telemetry as complete truth Important events may be missing. Combine traces, logs, metrics, and durable system state.
Ignoring monitoring gaps Missing telemetry can distort the incident explanation. Document what evidence was unavailable.
Producing follow-up actions unrelated to the cause Work is completed without reducing recurrence risk. Tie every action to a specific failure mechanism.
Only increasing capacity The same failure pattern returns at a larger scale. Fix amplification and isolation as well as capacity.
Skipping recovery analysis Long backlog drain or unstable recovery behavior remains unexplained. Analyze how the system returned to normal.

Production Checklist

  • Confirm user impact: quantify availability, latency, errors, or delayed processing.
  • Build a timeline: record system changes, alerts, failures, mitigations, and recovery.
  • Identify the first abnormal signal: distinguish initiating behavior from later symptoms.
  • Scope by region: determine whether the incident is localized.
  • Scope by version: compare current and previous deployments.
  • Scope by operation: identify which endpoints, queues, or workflows are affected.
  • Scope by dependency: separate internal and external failure domains.
  • Compare healthy traffic: use unaffected requests as a baseline.
  • Inspect tail latency: avoid relying only on averages.
  • Inspect waiting work: check queues, connection pools, and thread pools.
  • Inspect retries: measure amplification and total attempts.
  • Inspect timeouts: verify whether long waits are consuming capacity.
  • Inspect queue age: quantify delayed asynchronous processing.
  • Inspect dependency latency: identify slow downstream systems.
  • Inspect deployment history: correlate changes with the incident timeline.
  • Inspect configuration changes: include feature flags, limits, and routing changes.
  • Use traces: identify the critical path and retry behavior.
  • Use structured logs: inspect request-level context and error categories.
  • Use metrics: verify system-wide scope and resource saturation.
  • Use durable business state: verify workflow impact when telemetry is sampled.
  • Form explicit hypotheses: define what evidence should support or reject each one.
  • Document disproved hypotheses: prevent repeated investigation paths.
  • Identify amplification mechanisms: retries, fan-out, shared pools, or backlogs.
  • Identify missing isolation: inspect bulkheads, rate limits, and concurrency boundaries.
  • Analyze recovery: understand backlog drain and post-failure behavior.
  • Document telemetry gaps: identify missing logs, traces, or metrics.
  • Tie fixes to causes: every follow-up should reduce a specific recurrence path.
  • Test the failure mode: reproduce or simulate the root condition when safe.
  • Update alerts: add earlier signals revealed by the incident.
  • Update runbooks: preserve the fastest investigation path for future incidents.

Conclusion

Root cause analysis is not the process of naming the first component that failed. A useful RCA reconstructs the complete chain from trigger to user impact, identifies the mechanisms that amplified the failure, and explains why existing reliability controls did not contain it.

The most effective investigations combine timelines, metrics, traces, logs, deployment data, and durable application state. They use hypotheses rather than guesses, compare failing behavior with healthy baselines, and pay particular attention to waiting work, retries, saturation, and failure propagation across shared resources.

Key Takeaway: Find the causal chain, not just the broken component. Separate symptoms from triggers, prove hypotheses with evidence, identify amplification and missing isolation, analyze recovery behavior, and tie every follow-up action directly to a failure mechanism that contributed to the incident.

Author

Enjoyed this article?

Support Oleksandr Andrushchenko

Buy me a coffee

This helps Oleksandr Andrushchenko continue creating useful content

Related articles

Comments (0)