Production failures: the things that page you · Chapter 1 of 24 · Reliability · new chapter every Friday morning
By the end of this chapter: Read an unfamiliar dashboard under pressure and decide what to look at first.
Why this matters
When a pager goes off at three in the morning, adrenaline floods your system. Heart rate increases, cognitive focus narrows, and the brain defaults to pattern-matching rather than analytical reasoning. If you do not have a structured method for reading system state, this physiological response will force you down a rabbit hole. You will spend forty minutes investigating a minor CPU spike on a background worker while the primary database is completely unreachable.
Dashboards are almost always built by the engineers who wrote the application, which means they are designed to confirm the system is working, not to diagnose it when it is broken. They are dense with implementation details: garbage collection pauses, cache hit rates, queue depths, and thread counts.
Reading an unfamiliar dashboard under pressure requires ignoring ninety percent of what is on the screen. You must know exactly which three or four charts to look at first to isolate the fault domain. Without this discipline, you will guess at the root cause based on whichever chart looks the most erratic, prolonging the outage and exhausting yourself in the process.
Before you start
To execute the worked example and follow the technical reasoning in this chapter, you must have:
- Docker and Docker Compose installed on your local machine.
- Python 3.10 or higher installed.
- A basic understanding of HTTP status codes (specifically the difference between 200, 400, and 500 series).
- Familiarity with the concept of a time-series metric (a value recorded at a specific timestamp).
Bounding the problem from the outside in
The most common mistake in incident response is starting the investigation at the component you understand best, rather than the component closest to the user. If you are a database engineer, you look at database locks. If you are a frontend engineer, you look at the CDN.
You must evaluate the system from the outside in. The "outside" is the absolute edge of your infrastructure—usually a load balancer, an ingress controller, or an API gateway. The "inside" is the deepest layer of persistence.
When you open a dashboard, locate the metrics for the edge first. You are looking to answer a single, binary question: is traffic reaching our infrastructure at all? If the load balancer shows zero incoming requests, there is no point looking at application logs or database queries. The failure is upstream—perhaps a DNS failure, a revoked TLS certificate, or a severed transit link.
If traffic is reaching the edge, you move one layer inward to the application. If the application is receiving traffic but failing, you move to its dependencies. You only look at internal component metrics (like memory usage or thread counts) once you have proven that the layer above it is receiving requests and generating errors.
The three signals that dictate the investigation
Once you have identified the layer you are investigating, you must ignore custom business metrics and focus entirely on the RED signals: Rate, Errors, and Duration. These three metrics describe the experience of the consumer calling that layer.
Rate is the volume of requests per second. You are looking for sudden, sharp changes. A gradual increase is organic load; a vertical spike is a retry storm or a malicious attack. A sudden drop to zero means a pipe has broken upstream.
Errors are the rate of failed requests. In HTTP systems, this means 5xx status codes. You must explicitly filter out 4xx codes during initial triage. A spike in 4xx errors usually means a client deployed a bug and is sending malformed requests. A spike in 5xx errors means your infrastructure is failing to handle valid requests.
Duration is latency. You must never look at average latency. Averages hide catastrophic failures. If an endpoint serves 99 requests in 10 milliseconds, and one request hangs for 10,000 milliseconds before timing out, the average latency is 109 milliseconds. On a dashboard, 109ms looks like a healthy system. You must look at the 99th percentile (p99) latency. In that same scenario, the p99 latency is 10,000ms, which immediately tells you that a subset of users is experiencing total failure.
Correlating the signals to isolate the fault
Reading a dashboard is not about looking at one chart; it is about observing how Rate, Errors, and Duration move in relation to one another at the exact minute the alert fired. Their correlation tells you where to look next.
Scenario A: Rate remains flat, Duration spikes, Errors spike.
Traffic did not change, but suddenly the system got slow, and then it started throwing errors. This is the signature of resource exhaustion. A downstream dependency (like a database or a third-party API) slowed down. Your application threads waited for that dependency, holding open connections until they hit a timeout limit, at which point they returned 500 errors to the user. Do not look at application code; look at the downstream dependencies.
Scenario B: Rate spikes, Duration spikes, Errors spike.
This is a capacity failure. The system was overwhelmed by a sudden surge in traffic. The application ran out of CPU, memory, or network bandwidth trying to serve the load. The fix is usually to shed load, scale up, or block the offending traffic source.
Scenario C: Rate drops, Errors remain flat, Duration remains flat.
The system is fast and error-free, but nobody is using it. During peak hours, this is impossible. This means clients are failing to reach you. The fault domain is strictly upstream: DNS, BGP routing, or a misconfigured external firewall.
To see these correlations, you must control the time window of the dashboard. By default, dashboards often show the last 24 hours. A critical failure that began five minutes ago is invisible on a 24-hour graph; it looks like a single, tiny pixel. When you open the dashboard, immediately change the time window to start ten minutes before the alert fired, and end at the current time. This maximizes the visual contrast between "healthy" and "broken."
A worked example
This example simulates a production service that suddenly experiences a downstream dependency failure. We will spin up a Python application and a Prometheus instance to scrape its metrics. We will then trigger the failure and use PromQL (Prometheus Query Language) to triage the symptoms exactly as you would on a dashboard.
Create a directory named triage-example and create three files inside it.
1. app.py
This is our web server. It exposes metrics and a /simulate_outage endpoint.
import time
import random
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
from prometheus_client import start_http_server, Counter, Histogram
# RED Metrics
REQUEST_COUNT = Counter('http_requests_total', 'Total HTTP Requests', ['method', 'endpoint', 'status'])
REQUEST_LATENCY = Histogram('http_request_duration_seconds', 'HTTP Request Latency', ['endpoint'])
outage_active = False
class MetricsHandler(BaseHTTPRequestHandler):
def do_GET(self):
global outage_active
start_time = time.time()
if self.path == '/simulate_outage':
outage_active = True
self.send_response(200)
self.end_headers()
self.wfile.write(b"Outage simulation started.")
return
# Simulate normal traffic vs outage traffic
if outage_active:
# Downstream dependency is hanging, causing latency spikes and timeouts
time.sleep(random.uniform(2.0, 5.0))
status = 500
else:
# Normal operation
time.sleep(random.uniform(0.01, 0.05))
status = 200
REQUEST_COUNT.labels(method='GET', endpoint='/api/data', status=status).inc()
REQUEST_LATENCY.labels(endpoint='/api/data').observe(time.time() - start_time)
self.send_response(status)
self.end_headers()
self.wfile.write(b"Response")
def generate_background_traffic():
"""Simulates users constantly hitting the API."""
import urllib.request
while True:
try:
urllib.request.urlopen('http://localhost:8080/api/data', timeout=10)
except Exception:
pass
time.sleep(0.5)
if __name__ == '__main__':
# Start Prometheus metrics server on port 8000
start_http_server(8000)
# Start background traffic generator
threading.Thread(target=generate_background_traffic, daemon=True).start()
# Start main application server on port 8080
server = HTTPServer(('0.0.0.0', 8080), MetricsHandler)
print("Server running. Metrics on port 8000, App on port 8080.")
server.serve_forever()
2. prometheus.yml
This configures Prometheus to scrape our Python application.
global:
scrape_interval: 2s
scrape_configs:
- job_name: 'python_app'
static_configs:
- targets: ['app:8000']
3. docker-compose.yml
This wires the application and Prometheus together.
version: '3.8'
services:
app:
build:
context: .
dockerfile_inline: |
FROM python:3.10-slim
RUN pip install prometheus_client
COPY app.py .
CMD ["python", "app.py"]
ports:
- "8080:8080"
- "8000:8000"
prometheus:
image: prom/prometheus:v2.45.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
Execution and Triage:
- Open your terminal in the
triage-exampledirectory and run:docker-compose up -d - Wait 30 seconds for background traffic to generate baseline metrics.
- Open Prometheus in your browser at
http://localhost:9090. - In the query bar, type the following to check the Rate and click "Execute", then switch to the "Graph" tab:
rate(http_requests_total[10s])You will see a steady line representing normal traffic. - In a new terminal window, trigger the failure:
curl http://localhost:8080/simulate_outage - Wait 15 seconds.
- Now, triage the system using the RED method by running these three queries in Prometheus:
Query 1: Errors
rate(http_requests_total{status="500"}[10s])
You will see errors spiking from zero to a high rate.
Query 2: Rate (Total Traffic)
sum(rate(http_requests_total[10s]))
You will notice the total rate of requests has actually dropped slightly. The clients are blocked waiting for the slow server.
Query 3: Duration (p99 Latency)
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[10s]))
You will see latency jump from ~0.05 seconds to over 4.0 seconds.
By reading these three signals, you have successfully triaged the failure: Rate is steady/dropping, Duration spiked, Errors spiked. This is Scenario A. You know instantly that a downstream dependency is hanging and exhausting your application's capacity to serve new requests.
Where people get stuck
Symptom: You stare at a dashboard full of CPU and memory charts, trying to find the anomaly, but everything looks slightly jagged and you cannot tell what is normal.
Fix: Stop looking at resource metrics. Switch immediately to the load balancer or API gateway dashboard and look at the HTTP status codes and latency. CPU is a resource; it only matters if it impacts the user. Find the user impact first, then look at the resources of the specific component serving those users.
Symptom: The alert says the API is down, but the application dashboard shows zero errors and extremely low latency.
Fix: You are looking at the wrong fault domain. If the application shows no errors and low traffic, the traffic is not reaching the application. Move upstream. Check the ingress controller, the WAF (Web Application Firewall), or the DNS routing metrics.
Symptom: A customer complains that the system is timing out, but the dashboard shows an average latency of 45 milliseconds.
Fix: Change the dashboard aggregation from average (or mean) to p99 or p99.9. Averages smooth out outliers. If 99% of your traffic is fast and 1% is timing out at 30 seconds, the average will hide the failure entirely. You must measure the worst-case experience.
Your tasks
- Adjust the time window: In the Prometheus UI from the worked example, find the time controls (usually in the top right). Change the graph window from the default (1 hour) to 5 minutes. Observe how the transition from "healthy" to "broken" becomes much sharper and easier to read. 'Done' when you can clearly see the exact second the outage began on the graph.
-
Isolate the 4xx errors: Modify
app.pyso that whenoutage_activeis true, it returns a400status code instead of500, and keeps latency low (0.01s). Restart the docker container, trigger the outage, and write a PromQL query that proves the system is failing due to client errors, not server latency. 'Done' when your p99 latency query shows a flat line, but your error query shows a spike. -
Simulate an upstream failure: Modify the
generate_background_trafficfunction inapp.pyto completely stop sending requests whenoutage_activeis true. Restart the system, trigger the outage, and use PromQL to diagnose it. 'Done' when you can prove via therate()query that traffic has dropped to zero, while errors and latency remain flat.
Your tasks this week
Do the exercises above before the next chapter. Reading a tutorial and doing
one are different activities and only one of them changes what you can build.
Stuck on any of them? Say so — describe what you tried and what happened:
tell me where you got stuck. I read every one, and the questions
that come back more than twice get answered in the next chapter.
Production failures: the things that page you
Chapter 1 of 24. New chapter every Friday morning.
Next: Thundering herd: when the cache expires all at once.
· The full syllabus and every chapter so far
· Subscribers also get the condensed notes for this chapter, the running
recap of everything the series has covered, and the extended guidance:
subscribe
Written by Amit Chakraborty — founding engineer and senior architect: React Native, AI and RAG systems, production architecture. Portfolio · LinkedIn · GitHub.
Need this built, reviewed or taught to your team? Get in touch or email amit@devamit.co.in. Available for senior and founding engineering roles, consulting and training, remote worldwide.
Top comments (1)
The "dashboards are built to confirm health, not to diagnose failure" line is painfully accurate. I run automation across a fleet of VPS agents and my early dashboards were all green-lights: they answered "is the system up?" and were useless at 3am when the actual question is "what changed?".
What moved the needle for me: page on state transitions, not on error rates. A dependency that has been degraded for weeks is background noise; a component that was healthy 4 minutes ago and isn't now is almost always the story. Curious how you're structuring the 24 chapters — are you covering alert design itself, or keeping the scope to reading an existing dashboard under pressure?