Run post-incident reviews that produce genuine learning and systemic improvements instead of blame, politics, and action items nobody follows up on. Covers review facilitation, timeline construction, contributing factor analysis, action item quality, the difference between blame-free and accountability, and the process that turns incidents into your strongest competitive advantage.
A post-incident review is a structured, time-boxed session following a production outage, service degradation, or significant error—conducted within one to three business days after resolution. It aims to answer: *What happened? Why did it happen? What can we do differently next time?* It is not a blame assignment, but a learning engine for engineering teams. It matters when a failure impacts customers, slows release velocity, or reveals systemic gaps in design, operations, or communication.
To understand *what happened*, teams gather evidence from logs, metrics, and system state. The reconstruction begins with the incident’s first alert: a high-cardinality metric spike in Prometheus, a Slack alert from Datadog, or a cron job failure logged via systemd.
The first step is to extract the timeline of events from logs. Each log entry must include:
timestamp (ISO 8601, UTC)level (error, warning, info, debug)source (api, worker, cache, db, ingress)event (request_received, query_executed, timeout, deployment_started)A typical entry in the app.log file looks like:
{
"timestamp": "2024-03-15T14:23:17.123Z",
"level": "error",
"source": "worker",
"event": "job_failed",
"job_id": "7a2c1b5f-3d4e-6f7a-8b9c-0d1e2f3a4b5c",
"message": "Failed to process order #102345: timeout after 60s",
"context": {
"queue": "orders",
"worker_id": "worker-7",
"payload_size": 10240,
"dependencies": [
"payment-service",
"inventory-service"
]
}
}
To correlate logs across services, teams use a shared trace_id field in HTTP headers. The X-Trace-ID header is propagated through gRPC and HTTP calls. A failing request from the frontend to the order service includes:
GET /api/v1/orders/102345 HTTP/1.1
Host: api.example.com
X-Trace-ID: 7a2c1b5f-3d4e-6f7a-8b9c-0d1e2f3a4b5c
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
The trace_id is extracted in every service’s logging middleware. In Go, this is done via logrus and opentracing:
func WithTraceID(ctx context.Context, traceID string) context.Context {
return opentracing.ContextWithSpan(ctx, opentracing.StartSpan("request", opentracing.StartTime(time.Now()), opentracing.Tag{Key: "trace_id", Value: traceID}))
}
When a service logs a request, the trace_id is always present in the log line, enabling a full chain of events across services. Queries in Grafana or Loki can then group by trace_id to visualize the full request flow.
The root cause analysis begins with the *5 Whys* technique, but it is grounded in a specific diagnostic pattern: causal chain tracing.
Each incident starts with a known symptom: *Users cannot place orders*. The first investigation step is to identify the *first failure point*—where the system deviates from expected behavior.
From the logs, teams discover that the order service returns 504 Gateway Timeout after 60 seconds. They query the database for the orders table:
SELECT id, status, updated_at, processed_at
FROM orders
WHERE id = 102345
ORDER BY updated_at DESC
LIMIT 5;
The output shows that the processed_at field is stale—last updated 17 hours ago. This indicates that the background job responsible for processing orders is either down or delayed.
A systemctl status worker.service reveals that the worker daemon has been running, but its journal logs show:
$ journalctl -u worker.service -n 50 --since "2024-03-15 13:00:00"
The output shows that the worker failed to connect to the Redis cache after 13 hours of operation:
Mar 15 13:01:22 worker-7 redis: Failed to connect to redis: dial tcp 10.0.1.10:6379: connect: connection timed out
Mar 15 13:01:22 worker-7 redis: Retrying in 5s...
Mar 15 13:01:27 worker-7 redis: Retrying in 5s...
Mar 15 13:01:32 worker-7 redis: Retrying in 5s...
Mar 15 13:01:37 worker-7 redis: Retrying in 5s...
Mar 15 13:01:42 worker-7 redis: Retrying in 5s...
Mar 15 13:01:47 worker-7 redis: Failed after 10 retries
Mar 15 13:01:47 worker-7 worker: Redis unavailable; skipping 42 jobs
The root cause is not just *Redis is down*, but that the Redis instance on 10.0.1.10:6379 became unreachable after the worker started. The *causal chain* is:
GET /orders/102345 because the job is not processed.This chain is documented in a structured format: a causal map. It is not a diagram, but a list of events with causal links:
incident:
title: "Order Processing Failure"
start: "2024-03-15T13:00:00Z"
end: "2024-03-15T14:30:00Z"
symptoms:
- Users get 504s when placing orders
- Order list shows outdated status
- Dashboard shows high queue length in 'orders' queue
causal_chain:
- event: "Redis instance at 10.0.1.10:6379 becomes unreachable"
caused_by: "network partition between worker and Redis"
effect: "Worker fails to connect after 13h"
timestamp: "2024-03-15T13:01:22Z"
source: "worker-7"
- event: "Worker fails to process orders"
caused_by: "Redis unavailable"
effect: "Orders not updated in database"
timestamp: "2024-03-15T13:01:47Z"
source: "worker-7"
- event: "Order service times out on /orders/102345"
caused_by: "Order not processed"
effect: "Users see stale order data"
timestamp: "2024-03-15T14:23:17Z"
source: "api.example.com"
This causal map becomes the foundation for the incident report.
The core of the post-incident review is translating insights into actions. Each action must be:
Actions emerge from the causal chain and are written as action items in a shared document. Each item follows this template:
### [Action Item] Add alert for Redis connection failure
- **What**: When a worker fails to connect to Redis, send an alert to Ops Slack channel.
- **Why**: To detect early-stage Redis unavailability before order processing stalls.
- **How**:
- Update worker config to record `redis.connection_failed` metric.
- Use Prometheus to scrape worker metrics.
- Create alert in Alertmanager:
```yaml
groups:
- name: worker
rules:
- alert: RedisConnectionFailed
expr: rate(redis_connection_failed[5m]) > 0
for: 10m
labels:
severity: warning
annotations:
summary: "Redis connection failed on {{ $labels.worker_id }}"
description: |
The worker {{ $labels.worker_id }} has failed to connect to Redis for 10 minutes.
Last seen: {{ $value }}
Check: {{ $labels.worker_id }} logs and Redis health.
The **key insight** in action planning is that *not all failures are equally costly*. A high-frequency, low-impact failure is managed differently than a rare, catastrophic one. Teams use a **failure matrix** based on frequency and impact:
| Frequency | Low Impact | High Impact |
|-----------------|--------------------------------|----------------------------------|
| High | Monitor with logs | Automate response (runbook) |
| Low | Document in wiki | Add alert and incident playbook |
For example, the Redis connection failure is *high frequency, low impact*. The team decides to:
- Add a `redis_status` metric to the worker.
- Write a runbook: *"Recover from Redis outage"*, including:
- Check Redis health via `curl http://10.0.1.10:6379/health`
- Restart Redis with `systemctl restart redis.service`
- Clear Redis cache with `redis-cli FLUSHALL`
This runbook is stored in the `docs/incidents/runbooks/redis-outage.md` file and linked from the incident report.
## The Discipline of No-Blame
The review session is structured around a **no-blame pattern**, where the goal is not to assign fault, but to uncover systemic issues.
The session begins with a **timeline walk**:
- Team members read the causal chain aloud.
- Each event is discussed: *What was expected? What was observed? What was missing?*
The facilitator uses a **blameless retrospective board** with three columns:
1. **What Went Well**
2. **What Went Wrong**
3. **Systemic Patterns**
Each team member writes sticky notes (physical or digital) and places them in the appropriate column.
The **sharp edges** in this practice emerge from common failures:
- **Over-reliance on timestamps**: teams assume logs are synchronized, but NTP drift causes misaligned timelines. A 5-minute gap between worker and Redis logs is normal.
- **Confusing `error` and `warning` levels**: a `warning` in the log is often treated as a failure, but it is not. A `warning` that appears 10 times per minute is a signal of an ongoing issue, not a root cause.
- **The forgotten `trace_id`**: when a request is routed through multiple services, the `trace_id` is sometimes dropped in HTTP headers. The first service sets it, but the second service does not propagate it, breaking the chain.
A common misstep is **assuming a failure is isolated when it is systemic**. For example, the Redis outage is not just a network issue—it is a symptom of:
- Inadequate monitoring of Redis health (no `ping` probe)
- Lack of automatic failover (no Redis Sentinel)
- No alerting for high `latency` or `connection_errors`
Thus, the review leads to *systemic actions*, not just fixes.
## The Structure of a Review Session
A typical session lasts 90 minutes and follows this agenda:
1. **Timeline Walk (20 min)**
- Read the causal chain aloud.
- Add clarifications and questions.
2. **Root Cause Deep Dive (25 min)**
- Discuss each causal link.
- Ask: *Could this have been prevented? How?*
3. **No-Blame Brainstorm (20 min)**
- Use the three-column board.
- Add patterns: *“We always miss the trace_id”*, *“We assume logs are in sync”*.
4. **Action Planning (20 min)**
- Prioritize actions.
- Assign owners.
- Set due dates.
5. **Review and Commit (5 min)**
- Final check: all actions are clear, specific, and assignable.
The outcome is a **final incident report**, published in a shared document with:
- Incident summary
- Timeline
- Causal chain
- Root cause analysis
- Action items
- Runbooks
- Links to logs, metrics, and configuration
## The Pattern, Not the Product
This entire process is not tied to a tool. It works whether teams use:
- **Prometheus + Alertmanager + Grafana**
- **Datadog + Slack + PagerDuty**
- **OpenTelemetry + Loki + Tempo**
- **Simple JSON logs + kubectl + psql**
The pattern is:
- Use a shared `trace_id` across services.
- Extract logs with structured JSON.
- Correlate events using timestamps and identifiers.
- Build causal chains from sequences of events.
- Translate insights into structured action items.
- Conduct a time-boxed, no-blame review.
The mechanics are:
- Logs are always `timestamp`-based.
- Traces are propagated via headers.
- Metrics are scraped regularly.
- Alerts are defined as expressions over time series.
- Actions are documented in a shared format.
This is the *post-incident review: learning from failure without blame*.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy