How to Write Runbooks That Actually Get Used During Incidents

How to Write Runbooks That Actually Get Used During Incidents

A runbook is a real-time system with a human in the loop. The human is the most unreliable component. Design for that fact or accept that your runbook is decorative.

I learned this at 2:47 AM on a Wednesday in Lagos, during a database failover that should have taken 90 seconds and took 47 minutes. The runbook was written by a consultant who had never operated anything in West Africa. Step 4 said: “Wait 30 seconds and verify replication status on the replica.” The replica was reachable only over a shared 4G APN with 800ms RTT under no load—and during the failover window, because failover windows are when everything on the network decides to talk at once, the RTT spiked to roughly 3 seconds with packet loss around 4%. The “30-second” verification step took 11 minutes. The primary was already gone. We were blind.

The consultant was not incompetent. The runbook was clean, well-structured, and had obviously been reviewed by someone who cared. But it assumed an operator who could reach the replica over a low-latency private link, type commands without typos induced by sleep deprivation, and trust that the network would cooperate during the most stressful 60 seconds of the quarter. Every one of those assumptions was wrong. None of them was written down.

What follows is a field guide to writing runbooks that survive contact with reality: sleep-deprived engineers, 3G tethers, terminals with no GUI, brownouts that might kill the machine in 30 seconds. The core thesis is that a runbook is not documentation—it is an embedded system with a human peripheral. You should engineer it the way you would engineer any safety-critical control loop under degraded conditions.

Why Most Runbook Advice Assumes a World That Does Not Exist

The canonical reference for site reliability engineering practice, the Google SRE book, devotes entire chapters to being on-call, effective troubleshooting, emergency response, and managing critical state. These chapters are excellent. Read them. But they describe an environment with private fiber between datacenters, dedicated SRE rotations, and automated remediation for most failure modes. When the book talks about a human executing a procedure, that human is typically at a workstation with a stable link, a second monitor showing a dashboard, and a teammate on chat.

That is not the world I operate in. I suspect it is not yours either. The last runbook I followed during a real incident was executed from a parking lot in Ikeja, on a laptop tethered to a phone whose battery was at 14%, while a UPS in the server room beeped its low-battery warning every 30 seconds. There was no dashboard. There was a terminal, an SSH session that dropped twice, and a colleague on a voice call reading me steps from a wiki page that kept timing out on load.

The Google SRE framework is still the right starting point. The discipline of treating incident response as a practiced activity with documented procedures—not ad-hoc heroics—is the correct mental model. The problem is that most teams stop at the framework and never adapt the procedures to their actual operating environment. A runbook that assumes a calm, well-rested operator with stable connectivity is not wrong. It is incomplete in a way that becomes dangerous at the exact moment it is needed.

The Lagos Failover: What the Runbook Said vs. What Happened

The original runbook for our PostgreSQL failover had seven steps. I will not reproduce it verbatim, but the structure was familiar:

  1. Confirm the primary is unreachable via pg_isready -h primary -p 5432.
  2. Check the replica’s pg_stat_replication to confirm it is receiving WAL.
  3. Promote the replica using pg_ctl promote -D /var/lib/postgresql/data.
  4. Wait 30 seconds and verify the replica accepts writes.
  5. Update the connection string in the application config.
  6. Restart the application pool.
  7. Verify the application can write.

Clean. Linear. Wrong for our environment in at least four ways.

First, step 1 assumed pg_isready would return quickly. Over the 4G link, a TCP connect to the primary took 3–8 seconds when the host was up, and 75 seconds (the default tcp_syn_retries timeout) when it was down. The runbook did not specify a timeout, so the operator waited the full 75 seconds, concluded the primary was down, and moved on. Correct in this case, but only by accident. If the primary had been up but slow, the operator would have promoted a replica against a live primary.

Second, step 2 assumed the operator could query the replica. The replica was in a secondary datacenter reachable over the 4G APN. The pg_stat_replication query—trivial on a LAN—took 11 minutes over the degraded link because the SSH session had to be re-established twice and the psql client’s default connect timeout was 60 seconds, which was sometimes not enough.

Third, step 4 said “wait 30 seconds and verify.” Verify how? The runbook did not say. The operator ran SELECT 1; and it returned. But SELECT 1 does not confirm the node is a primary—it confirms the node is alive. The operator needed SELECT pg_is_in_recovery(); returning false. The runbook omitted this.

Fourth, step 5 said “update the connection string in the application config.” Which file? On which hosts? Via what mechanism? The runbook assumed a config management tool that could push changes. Our config management ran from the same datacenter as the primary, which was down.

Every one of these gaps was survivable in a review meeting. Every one was fatal at 2:47 AM.

A Framework: Runbooks as Embedded Firmware Documentation

After that incident, I rewrote every failover runbook using a mental model borrowed from embedded systems. Treat the runbook as firmware documentation for a control loop where the actuator is a sleep-deprived human and the sensor is a degraded network. In embedded systems, you do not write “set the register and check the flag.” You write: “Write 0x01 to register CTRLA. Within 10ms, read STATUS. If bit 3 is set, the device is ready. If STATUS reads 0xFF, the bus is hung—power-cycle the device and retry from step 1. Do not proceed past this step if STATUS is not 0x01 or 0x02 within 50ms.”

Every step has a precondition, a timeout, an expected result, a failure mode, and a “stop here if” branch. The human never has to decide what to do next under stress. The decision tree is already in the document.

The rewritten runbook has the following structure for each step:

  • Precondition: What must be true before you start this step. If it is not true, do not proceed—go to the specified fallback.
  • Command: The exact command, with all flags, including timeouts. No psql without -c. No ssh without -o ConnectTimeout=10.
  • Expected: The exact output you should see, or a pattern. Not “it should work”—the literal string or a regex.
  • If unexpected: What to do if the output does not match. Usually: stop, try once more with a longer timeout, then go to a named fallback step.
  • Time budget: How long this step should take under normal conditions and under degraded conditions. If it exceeds the degraded budget, something is wrong. Do not keep waiting.
  • Stop here if: The conditions under which you must halt the entire procedure and escalate. This is the circuit breaker for the human.

Here is what step 1 of the failover runbook looks like now:

Step 1: Confirm primary is unreachable.

Precondition: You have SSH access to the replica host. If you do not, go to Fallback A (manual promotion from console).

Command:

timeout 15 pg_isready -h <primary_ip> -p 5432 || echo "UNREACHABLE"

Expected: Output containing no response or UNREACHABLE within 15 seconds.

If you see: accepting connections — the primary is up. Stop here. Do not promote. Go to Fallback B (investigate application-level failure instead).

If the command takes longer than 15 seconds: The network is degraded. Note the time. Proceed to step 2 but expect all subsequent steps to take 3–5× longer than the time budget.

Time budget: Normal: 2 seconds. Degraded: 15 seconds. If it takes longer than 15 seconds, your SSH session may also be unstable—reconnect before proceeding.

Stop here if: You cannot establish SSH to the replica at all. Go to Fallback A.

That is more verbose than the original. It is also executable by someone who has never done a failover before, at 3 AM, over a bad link, without making a decision that destroys data.

Encoding Network Assumptions as Explicit Preconditions

The single most important change in the rewrite was making network assumptions explicit. The original runbook assumed the operator could reach the replica over a low-latency private link. That assumption was never written down, so it was never tested, so it failed silently.

In the rewritten runbook, every step that involves a network call has a precondition stating the expected network characteristics and what to do if they are not met. For the Lagos failover, step 2 now reads:

Step 2: Check replication status on replica.

Precondition: RTT to the replica is under 2 seconds. Test with:

timeout 10 ssh -o ConnectTimeout=10 replica_host 'echo OK'

If this does not return OK within 10 seconds, you are on a degraded link. All time budgets in this runbook are now multiplied by 5. If the command does not return within 30 seconds, proceed to Fallback C (promote blind, accept data loss, reconcile later).

This is uncomfortable to write. “Promote blind, accept data loss” is not something anyone wants to put in a runbook. But the alternative is worse: an operator who spends 11 minutes on a verification step while the primary is gone and the application is hard-failing, then makes a panicked decision with no structure at all. Explicit failure modes are not pessimism. They are honesty. Structured frameworks for operational risk management, like the NIST Cybersecurity Framework, make response and recovery explicit, versioned, and profile-based precisely because implicit knowledge breaks under stress. Your runbook should do the same.

Verification Steps That Degrade Gracefully

The original runbook’s verification step (“wait 30 seconds and verify”) had two problems. It did not say what to verify, and it assumed the verification would complete in 30 seconds. The rewrite addresses both.

For verification, every step now specifies the exact query and the exact expected output. For the failover, the verification is:

psql -h replica -p 5432 -c "SELECT pg_is_in_recovery();"

Expected: f (false). If you see t, the promotion did not work—go to Fallback D. If the query times out, the node is alive but overloaded. Wait 60 seconds and retry once. If it times out again, go to Fallback C.

For degradation, every time-bound step now has two budgets: a normal budget and a degraded budget, with the degraded budget typically 5× the normal. The runbook states that if any step exceeds the normal budget, the operator should note it and expect all subsequent steps to be similarly slow. This prevents the failure mode where an operator assumes the next step will be fast because the last one was, then makes a timing-based decision on bad data.

Testing Runbooks Over Throttled Connections

A runbook you have never executed under degraded conditions is a hypothesis, not a procedure. After the Lagos incident, I started testing every runbook over a throttled connection before it was considered production-ready.

The test setup is simple and crude, which is the point:

  • Connect to the network you actually use during incidents. For us, that is a 4G APN with intentional throttling applied via tc qdisc on the test machine: 256 Kbps, 800ms RTT, 2% packet loss.
  • Execute the runbook end to end, timing each step. Record where it exceeds the degraded budget.
  • Have someone who did not write the runbook execute it. Watch where they hesitate, where they ask questions, where they improvise. Every hesitation is a missing precondition. Every question is a missing command specification. Every improvisation is a missing fallback.

The first time I ran the failover runbook through this test, step 2 took 9 minutes. The runbook said 30 seconds. The gap was not a bug in the runbook—it was an unstated assumption about the network. The test made the assumption visible. The rewrite made it explicit.

I also test runbooks with a second constraint: the operator cannot use a GUI. No dashboards, no web-based wiki, no browser tabs. Only a terminal and a local copy of the runbook as a plain text file. If the runbook requires a GUI to execute, it fails the test. This is not because GUIs are bad. It is because GUIs are the first thing to go when the network is degraded, and a runbook that depends on a Grafana dashboard is a runbook that cannot be followed when Grafana takes 45 seconds to load a panel.

Structuring the Document: Why I Do Not Use Wikis for Runbooks

Wikis encourage drift. Multiple editors, no linear structure, no review gate, no version that is “the” version. A runbook in a wiki is a pile of bullet points that someone edited once at 4 PM on a Tuesday and that someone else will try to follow at 3 AM on a Saturday. The two experiences are not the same document.

I draft runbooks as structured, versioned documents—plain text or Markdown in a Git repo, reviewed via pull request, with a single canonical version per incident type. The discipline of writing a runbook as a coherent document, not a wiki page that grew organically, forces you to think about narrative flow: does step 3 make sense if step 2 was degraded? Does the fallback path actually reconnect to the main path, or does it dead-end? These are questions that a bullet-point wiki does not force you to answer.

I have started drafting the narrative prose for new runbooks in a structured writing tool rather than a wiki. The act of composing a runbook as a document—with preconditions, decision trees, and fallback paths that must be internally consistent—is closer to writing a technical specification than maintaining a knowledge base. I use an AI novel writing app for the initial draft pass, not because it generates runbook content—it does not, and you should not let it—but because the structured outlining and chapter-based workflow forces the same discipline that a coherent technical document demands: every section has a purpose, every branch has a destination, every fallback has a name. The tool is for structure, not content. The content comes from incident experience, postmortem findings, and the throttled-connection tests described above. What the tool provides is the scaffolding that prevents the document from devolving into a wiki-style pile of loosely connected notes.

That same discipline applies to editorial structure: before publishing, editors need a way to test scattered notes become an argument readers can follow, which is where an AI novel writing app that fits the project can function as a planning aid rather than a substitute for domain evidence.

The key principle: a runbook should be a single, reviewable artifact with a version, an owner, and a test date. Not a living document that anyone can edit at any time. Living documents die during incidents.

The Postmortem Feedback Loop

Every runbook that is used during a real incident must be reviewed afterward. This is not optional. It is not a nice-to-have. The review answers three questions:

  1. Did the runbook work? Did every step complete within its degraded budget? Did every fallback path get used, and did it work?
  2. What assumptions did the runbook make that turned out to be wrong? Every unstated assumption that bit you during the incident gets written as an explicit precondition in the next version.
  3. What did the operator do that was not in the runbook? Every improvisation is a missing step or a missing fallback. Add it.

This is the same feedback loop that postmortem culture in SRE practice prescribes, applied specifically to the runbook as an artifact. The postmortem is about the system. The runbook review is about the document that tells you how to operate the system. Both are necessary. A postmortem without a runbook review means you understand what went wrong but have not updated the procedure for next time.

Checklist: Runbook Readiness for Degraded Conditions

  • Every step has a precondition. If the precondition is not met, the step says where to go instead.
  • Every command includes explicit timeouts. No command relies on default timeouts.
  • Every step specifies the expected output as a literal string or pattern, not “it should work.”
  • Every step has a normal time budget and a degraded time budget (typically 5× normal).
  • Every step has a “stop here if” condition that halts the procedure and triggers escalation.
  • Every fallback path is named (Fallback A, B, C) and has its own steps, not just “figure it out.”
  • The runbook has been executed end to end over a throttled connection (256 Kbps, 800ms RTT, 2% loss) by someone who did not write it.
  • The runbook has been executed without a GUI—terminal only, local copy as plain text.
  • The runbook has a version, an owner, and a last-tested date in the document header.
  • After every real incident, the runbook is reviewed and updated within 72 hours.

A runbook is not documentation. It is a control system. Engineer it the way you would engineer anything that has to work when everything else is failing: with explicit assumptions, explicit failure modes, and explicit decision trees that a sleep-deprived human on a bad link can follow without thinking. If your runbook cannot be followed at 3 AM over 4G during a brownout, it is not a runbook. It is a wish.

Debugging Intermittent Failures When Your Infrastructure Is Held Together with Hope

Intermittent failures are the ghost in the machine that only shows up when you’re not looking. For a systems engineer working outside the glass-and-steel data centers of Frankfurt or Virginia, the ghost is more of a poltergeist. We’re not just chasing a race condition in a microservice. We’re chasing a voltage drop on a shared phase, a microwave link that fades when the afternoon rain hits, or a diesel generator that decides its automatic transfer switch is merely a suggestion. An intermittent failure is a transient, non-reproducible-on-demand fault that corrupts a service’s expected state. It sits at the intersection of software logic, hardware physics, and environmental chaos. In environments with constrained resources—unreliable grid power, oversubscribed backhaul, aging server hardware pushed years past its vendor support contract—these failures aren’t edge cases. They are the main case. Understanding them means moving beyond a pure software stack trace and into a full view of the entire socio-technical system, from the kernel’s Out-Of-Memory (OOM) killer to the diesel mechanic who forgot to tighten a fuel line.

A technician's hands working on a complex wiring panel in a dimly lit server room, symbolizing the physical-layer debugging required for intermittent failures.
Debugging often starts not with a log, but with a physical inspection of the infrastructure that silently underpins your uptime.

Why “It Works on My Machine” Is a Dangerous Illusion Here

In a well-provisioned cloud environment, you can often assume the underlying fabric is reliable. You might blame the network, but deep down you trust the hypervisor’s clock source and the switch’s buffer. In our context, that trust is a liability. I’ve spent weeks tracking a database replication lag that occurred only between 2:00 PM and 4:00 PM. The logs showed nothing but normal I/O wait. The culprit? A nearby industrial bakery sharing our building’s transformer would fire up its massive ovens at that time, causing a harmonic distortion on the power line that subtly slowed the server’s CPU clock cycle, which in turn desynchronized the database’s internal timing for write-ahead logs. The software was fine. The physics were not.

This reality forces a different debugging methodology. You cannot rely solely on application performance monitoring (APM) tools, which often assume a stable base layer. You need a layered approach that correlates software events with physical-world timestamps and environmental metrics. The goal is to transform an invisible, transient fault into a visible, reproducible pattern by widening the observation surface.

Building a Correlation Engine from Scraps

The core strategy is to overlay multiple independent data streams onto a single timeline. When a user reports a 502 error at “roughly 11:15 AM,” you need to know what else happened at 11:15 AM. Not just in Nginx, but in the kernel, on the switch, and on the wall socket. Here is a practical, low-cost stack I’ve assembled across multiple deployments in West Africa and South Asia.

1. The Software Layer: Beyond the Stack Trace

Standard application logs are often useless for intermittent failures because the error is a symptom, not a cause. A Python process dying with a Killed message tells you nothing. You need the kernel’s story. Enable and ship the kernel ring buffer (dmesg) to your central logging system. The OOM killer’s scoreboard is your first truth. I use a simple rsyslog configuration to forward kern.* messages to a central Loki instance, which is lighter than Elasticsearch and runs happily on a Raspberry Pi or an old Dell R610.

Next, instrument the TCP stack. Intermittent network timeouts are often caused by buffer bloat or retransmission storms that don’t show up in application logs. A lightweight eBPF script or a simple ss -ti cron job logging to a file can capture the smoothed RTT and retransmission count for your critical connections. When a user sees a timeout, check if the kernel’s TCP retransmit counter spiked at that exact second. This data is gold and costs you nothing in CPU overhead.

2. The Physical Layer: The $20 Debugger

You cannot debug what you cannot measure. For every critical server or network appliance, I deploy a separate, low-power monitor—usually a Raspberry Pi Zero or an old Android phone running Termux. This monitor does three things:

  • Pings the server’s internal IP every second and logs the latency and packet loss to a CSV file.
  • Monitors the mains voltage and frequency using a cheap USB power meter (or a custom ADC sensor if you’re handy with a soldering iron). A voltage sag to 190V on a “220V” line is a common prelude to a server’s PSU crowbar circuit tripping.
  • Logs ambient temperature and humidity via a DHT22 sensor. I’ve seen a Cisco switch start corrupting packets when the intake air hit 55°C because the dust filter was clogged, but the switch’s own internal sensor was poorly calibrated and reported a safe 40°C.

All this physical data gets shipped to the same Loki instance with a source=physical label. Now, when a software error occurs, you can instantly pull up the physical world state at that timestamp. No more guessing about “dirty power.” You have a graph.

A close-up of a digital multimeter measuring voltage on a circuit board, representing the need to verify electrical inputs during debugging.
Verifying the electrical reality against the software’s assumptions is a non-negotiable first step in intermittent failure analysis.

Case Study: The Vanishing LTE Backhaul

Let me walk through a real debugging session that illustrates this layered approach. We had a remote site using an LTE router as a failover WAN link. The primary fiber would drop, the router would fail over to LTE, and everything would work for about 90 seconds. Then, the VPN tunnel would collapse, and the router would reboot. The logs showed a clean PPP disconnection followed by a modem reset. The telco’s NOC insisted the signal was “excellent.”

We deployed a physical monitor. The data showed that the moment the router switched to LTE, the 12V DC power supply’s voltage dropped from 12.1V to 10.8V. The router’s internal modem, when transmitting at full power to reach a distant tower, drew a current spike that the aging power brick couldn’t handle. The voltage sag caused the modem’s chipset to brown out, triggering a firmware reset. The fix wasn’t a software patch or a new router; it was a $15, 3-amp power supply with a thicker gauge DC cable. The software logs were a distraction. The physical layer was the root cause.

Methodology: The Hypothesis-Driven Blame Game

When you have limited time and no spare hardware for a staging environment that perfectly mirrors production, you cannot afford to “try things and see what happens.” You must be surgical. I follow a strict, blame-oriented debugging protocol adapted from the medical differential diagnosis model.

Step 1: Define the Failure Signature Precisely

“The website is slow” is not a signature. “A GET request to /api/orders from a client on the 192.168.3.0/24 subnet takes longer than 5 seconds, but only between 14:00 and 16:00 UTC, and only when the response payload exceeds 50KB” is a signature. The precision forces you to look at the specific components in the path: the subnet’s switch, the time-of-day cron jobs, the server’s memory pressure when serializing large objects. Narrow the blast radius before you start digging.

Step 2: List Every Component in the Critical Path

Draw the full path on a whiteboard. For that API call, the path includes: client browser, client OS TCP stack, office Wi-Fi AP, office switch, microwave backhaul radio, ISP’s core router, your firewall, your reverse proxy, your application server’s network interface, the OS network stack, the web server process, the application code, and the database connection. Do not skip the client’s Wi-Fi AP. I once found that an office’s AP was rebooting every hour due to a PoE injector fault, causing exactly the intermittent pattern we saw on the server side.

Step 3: Inject a Non-Intrusive “Canary” Probe

You need a control signal. Write a tiny script that mimics the failing transaction but is simpler and has fewer dependencies. If your main app uses a complex ORM, the canary should use a raw socket or a minimal HTTP client. Run it from the same client subnet and from a different one. If the canary fails at the same time as the main app, the problem is in the network or the OS, not the application code. This is the single most effective triage step I know, and it requires no expensive tooling.

Tools That Don’t Require a Budget Line Item

Forget the expensive APM suites that assume a Kubernetes cluster with 32GB nodes. Here are the tools that actually work when your server has 2GB of RAM and a spinning disk.

  • atop with process accounting: Unlike top, atop logs raw process-level metrics to disk and lets you replay a specific time window after a crash. It shows you which process was hogging I/O exactly when the failure occurred, even if that process has since exited. Essential for catching OOM situations or short-lived cron job spikes.
  • mtr (My Traceroute): A continuous traceroute that shows packet loss and latency per hop over time. Run it between your server and a critical upstream (like your DNS resolver or payment gateway) and log the output. Intermittent routing loops or ISP congestion become visible as a pattern, not a one-off traceroute snapshot.
  • sysdig for system call tracing: When you suspect a file descriptor leak or a short-lived process that opens a socket and dies, sysdig with a chisel can capture every connect(), open(), and kill() on the system with minimal overhead. It’s like a security camera for your kernel.
  • Wireshark on a mirror port: If you have a managed switch, set up a port mirror to a laptop running Wireshark. Capture traffic for 24 hours. Look for TCP retransmissions, duplicate ACKs, or ARP storms that coincide with the failure times. A surprising number of “application hangs” are caused by a faulty NIC flooding the subnet with pause frames.
A laptop screen displaying a network protocol analyzer with multiple packet streams, used for deep-dive debugging of intermittent network issues.
A protocol analyzer is not a luxury; it’s a necessity when your ISP’s “guaranteed” SLA is a handshake agreement.

The Human Factor: When the System Isn’t the Problem

In resource-constrained environments, the most unpredictable component is often the human operator. I’ve debugged a “random” database corruption that happened every Tuesday. The logs showed a clean shutdown, then corruption on restart. The physical monitor showed a voltage spike every Tuesday at 10:00 AM. The cause? The office cleaner unplugged the server rack to plug in a floor polisher, then plugged it back in. The server’s UPS batteries were dead, so the machine hard-powered off. The database’s journaling wasn’t configured for that abuse. The fix was a combination of a new UPS battery, a locked power socket, and a conversation with the cleaning staff. No amount of software engineering would have solved this. You must talk to people, observe the physical space, and understand the local rhythms of life and work.

Designing for Degradation, Not Perfection

Ultimately, debugging intermittent failures in production is a reactive strategy. The proactive approach is to design systems that assume components will fail intermittently. This means implementing circuit breakers, retries with exponential backoff and jitter, and graceful degradation. If your payment gateway is unreachable, queue the transaction locally and retry. If your primary database is slow, serve stale reads from a local cache. These patterns are well-documented, but they require a mindset shift: you are not building a system for a perfect world. You are building a system for a world where the power is dirty, the network is congested, and the hardware is tired. This is the reality of systems engineering in Africa, South Asia, and Latin America, and it’s a reality that produces some of the most resilient engineers on the planet.

Frequently Asked Questions

What’s the first thing I should check when a production service fails intermittently?

Start with the physical layer. Check power stability, network link status, and hardware health (disk SMART data, CPU temperature). In non-ideal environments, these are the most common culprits and the easiest to rule out before diving into complex software traces. A simple script logging voltage and ping latency can save days of investigation.

How do I debug an issue that only happens once a month?

You need persistent, low-overhead logging that captures the system state continuously. Use atop to record process-level metrics, mtr for network path data, and a physical monitor for environmental conditions. When the failure occurs, you can replay the data around that timestamp. The key is to have the data already recorded; you cannot start logging after the fact.

Is it worth investing in expensive monitoring tools for a small deployment?

Usually, no. The open-source tools mentioned here (Loki, atop, sysdig, Wireshark) are free and run on minimal hardware. The real investment is the time to set them up and learn to interpret the data. A $20 Raspberry Pi monitor and a well-configured rsyslog server will give you 80% of the insight of a commercial APM tool, without the licensing cost or resource overhead.

Debugging Intermittent Production Failures: A No-Nonsense Guide

Intermittent failures in production are the worst. They don’t show up in staging. They don’t reproduce on your machine. They strike at 3 a.m., trigger a PagerDuty alert, and vanish before you can even rub the sleep from your eyes. If you’ve spent any real time in the trenches of backend engineering, you know these aren’t just annoying—they erode user trust and burn real money. This guide skips the hand-waving. We’ll talk about concrete strategies, the tools you actually need, and the mindset that catches transient failures before they catch you.

Why Intermittent Failures Are Different

Most bugs are deterministic. You feed the system a specific input, it produces a wrong output. You fix the logic, you’re done. Intermittent failures don’t play that game. They’re probabilistic. They depend on a constellation of factors: race conditions, resource exhaustion, network timeouts, garbage collection pauses, or even a cosmic ray flipping a bit in memory. The system works fine 99.9% of the time, but that 0.1% can cascade into a full-blown outage.

Engineers often waste hours trying to reproduce these failures in a cozy development environment. Don’t. Your laptop isn’t production. The network isn’t saturated, the database isn’t under load, and the clock isn’t drifting. Accept that early, and you’ll save yourself a lot of frustration.

Instrument Before You Investigate

You can’t debug what you can’t see. If your production system doesn’t emit detailed telemetry, you’re flying blind. The foundation of any intermittent failure investigation is observability—logs, metrics, and traces. But not just any logs. You need structured logging with correlation IDs that follow a request across every service boundary. Without a trace ID, a timeout in a microservice mesh is just a needle in a stack of needles.

Metrics should capture percentiles, not just averages. A p99 latency spike that lasts two seconds won’t budge the average, but it will cause a handful of requests to fail. Use histograms. Track error rates by endpoint, by status code, and by upstream or downstream dependency. When an alert fires, your first question should be: “What changed?” If your dashboards only show smoothed-out aggregates, you won’t have an answer.

Patterns That Cause Intermittent Failures

Over the years, I’ve seen the same patterns repeat across different stacks and architectures. Recognizing them speeds up diagnosis.

1. Resource Starvation Under Load

Your service handles 1,000 requests per second without breaking a sweat. Then traffic climbs to 1,200, and suddenly requests start timing out—but only every few minutes. CPU isn’t maxed, memory is fine, disk I/O looks normal. The real culprit is often thread pool exhaustion or connection pool saturation. A downstream service slows down just enough to block threads, which backs up the pool, which causes timeouts upstream. Adding more threads can make it worse. Sometimes the fix is shorter timeouts or circuit breakers that let slow dependencies fail fast instead of clogging the system.

2. Garbage Collection Pauses

In managed runtimes like the JVM or .NET CLR, garbage collection can introduce latency spikes. A full GC pause of 200 milliseconds might be rare, but if it happens during a request with a 100ms timeout, you’ve got a failure. Modern collectors like G1 or ZGC reduce pause times, but they don’t eliminate them. Correlate GC logs with request latency. If you see a spike in GC activity coinciding with errors, you’ve found your culprit. Tune heap sizes, object allocation rates, or switch collectors.

3. Network Blips and Retry Storms

A single dropped TCP packet can cause a cascade if your retry logic is aggressive. I once debugged a system where a 0.1% packet loss rate led to a 5% error rate because every failed request triggered three retries, each of which could also fail. The solution was exponential backoff with jitter, not more retries. Use a library like Polly for .NET or resilience4j for Java to implement battle-tested retry and circuit breaker patterns.

4. Database Deadlocks and Lock Escalation

Intermittent deadlocks in a relational database are a classic sign of lock ordering issues. Two transactions grab locks in different orders, and under high concurrency, they collide. The database engine picks a victim and rolls it back. Your application sees a transient error. The fix is to enforce a consistent lock acquisition order across all transactions. For NoSQL stores, watch out for optimistic concurrency control failures—retry with exponential backoff.

5. Time and Clock Drift

Distributed systems rely on clocks for ordering, expiration, and coordination. If one server’s clock drifts by a few seconds, tokens expire prematurely, or leader election goes haywire. NTP misconfiguration is a silent killer. Monitor clock skew across your fleet. If you’re using leases or TTLs, ensure your logic tolerates reasonable drift—don’t assume everyone agrees on what “now” means.

Building a Debugging Toolkit

When an alert fires, you need immediate access to the right data. Here’s what I keep at my fingertips:

  • Centralized logging with full-text search and field-based filtering. ELK stack, Grafana Loki, or Splunk—pick one and invest in log structure.
  • Distributed tracing to visualize request flows. Jaeger or Zipkin can show you exactly where latency spikes occur.
  • Metrics dashboards with granular time ranges. I use Grafana with Prometheus, configured to show p50, p95, and p99 latencies per endpoint.
  • Heap dumps and thread dumps for JVM-based services. Capture them automatically when memory or thread thresholds are breached.
  • Database query logs with slow query analysis. Enable the slow query log in MySQL or use pg_stat_statements in PostgreSQL.

Reproducing the Unreproducible

You can’t fix what you can’t reproduce, but you can get close. Chaos engineering isn’t just for testing resilience—it’s a debugging tool. Inject latency, packet loss, or resource constraints into a staging environment that mirrors production. Use tools like Toxiproxy or Gremlin to simulate network conditions. If you can trigger the failure under controlled chaos, you can instrument the code path and capture the exact state when it breaks.

Another approach: replay production traffic. Tools like GoReplay or tcpreplay let you capture and replay real requests against a debug instance. This is especially useful for race conditions that depend on specific interleavings of concurrent requests.

Case Study: The Vanishing Database Connection

Let me walk through a real debugging session. A service I maintained would throw sporadic “connection closed” errors from the database pool. The errors occurred roughly once every two hours, with no correlation to traffic volume. The database logs showed nothing unusual—no restarts, no connection limits hit. The application logs showed the connection was being closed by the server, not the client.

I enabled TCP keepalive logging on the application hosts and discovered that the idle connections were being dropped by a stateful firewall between the app and the database. The firewall had a 30-minute idle timeout, but the connection pool’s idle timeout was set to 60 minutes. Connections sat idle for 30 minutes, got killed by the firewall, and the pool didn’t detect the dead connection until it tried to use it. The fix was simple: set the pool’s idle timeout to 25 minutes and enable connection validation on borrow.

Checklist for Intermittent Failure Investigations

When you’re staring at an alert at 2 a.m., follow this checklist. It won’t solve every problem, but it will keep you from chasing ghosts.

  1. Narrow the time window. Pinpoint the exact timestamps of failures. Correlate with deployments, config changes, or traffic spikes.
  2. Identify the failing component. Is it the application, the database, a cache, a queue, or the network? Use distributed traces to isolate the boundary.
  3. Check resource saturation. CPU, memory, disk I/O, network bandwidth, file descriptors, thread pools, connection pools. Look for plateaus or cliffs.
  4. Examine error logs at the failure boundary. Don’t just look at your app—check the logs of the database, load balancer, and service mesh.
  5. Review recent changes. Even a minor config tweak can introduce a race condition. Git blame is your friend.
  6. Test hypotheses in isolation. If you suspect a connection pool issue, write a script that simulates the pool behavior under load.
  7. Add targeted instrumentation. If existing telemetry is insufficient, add temporary detailed logging or metrics and redeploy.

Prevention: Designing for Transience

The best way to debug intermittent failures is to prevent them from becoming incidents. Design your system to tolerate transient faults gracefully.

  • Implement retries with exponential backoff and jitter. This prevents thundering herds and retry storms.
  • Use circuit breakers. If a downstream service is failing, stop calling it for a while to let it recover.
  • Set appropriate timeouts. Timeouts should be shorter than the client’s expected response time but long enough to allow normal operation.
  • Add idempotency keys. If a client retries a request, the server should recognize it and avoid duplicate processing.
  • Test for chaos. Regularly inject failures in staging to ensure your system degrades gracefully.

FAQ

Why can’t I reproduce the issue in my development environment?

Development environments lack the concurrency, network latency, and resource constraints of production. Intermittent failures often arise from race conditions that only manifest under high load or specific timing conditions. Use production-like staging environments with traffic replay to get closer to the real scenario.

How do I know if the problem is in my code or the infrastructure?

Distributed tracing is your best tool. If the trace shows a timeout or error at the boundary between your service and a database, cache, or external API, the infrastructure is likely the cause. If the error occurs within your service’s logic, it’s a code issue. Check for exceptions, deadlocks, or resource exhaustion in your service’s logs.

What’s the quickest way to mitigate an intermittent failure while I debug?

If the failure is causing user impact, don’t wait for a root cause. Restart the affected service or roll back to the last known good deployment. Increase timeouts or retry budgets temporarily. If it’s a database issue, fail over to a replica. Mitigate first, then investigate with the pressure off.

Closing Thoughts

Intermittent failures punish sloppy engineering. They expose gaps in your observability, your testing, and your understanding of the system. Every time you track one down, you’ll emerge with a deeper knowledge of how your stack actually behaves under stress—not how you think it behaves. That’s the silver lining. Keep your tools sharp, your logs structured, and your assumptions challenged. The next 3 a.m. alert won’t stand a chance.

Server rack with blinking lights in a dark data center

Close-up of network cables plugged into a switch

Engineer analyzing server logs on multiple monitors

Chasing Ghosts in Production: A No-Nonsense Guide to Debugging Intermittent Failures

You push a service. All tests pass. Monitoring is a sea of green. Then, at 2:17 AM on a Tuesday, your phone screams. By the time you’re awake enough to look, the error is gone. The logs show a single 500, then silence. Welcome to the special hell of intermittent production failures. These aren’t the clean, reproducible bugs you squash in dev. They’re statistical gremlins that surface under weird load, odd timing, or hardware states you can’t easily replicate. Debugging them demands a different headspace: you’re not just a code fixer anymore. You’re a detective, a physicist, and a deeply paranoid sysadmin all at once. This guide lays out a blunt, technical framework for hunting these ghosts—drawn from too many 3 AM pages.

1. Stop Looking at the Code First

Your fingers itch to check the recent commits. Don’t. An intermittent failure is a function of state, not just logic. The code ran fine a million times before it didn’t. Start by characterizing the failure’s shape. Open your dashboards and ask: What’s the exact temporal pattern? Is the failure pinned to a specific host, availability zone, or instance type? Does the error rate track with something else—request latency, CPU steal time, GC pauses, or upstream service response times? A failure that spikes every 57 minutes hints at a cron job or cache expiry. A failure stuck to one container suggests a bad node, not a bad algorithm. Write the answers down. If you can’t answer these, you don’t have a debugging problem yet—you have an observability gap. Close that gap first.

Close-up of a server rack with blinking lights, representing the physical infrastructure where intermittent failures often originate

2. Instrument the Edges, Not the Core

Most intermittent failures lurk at the boundaries: network calls, disk I/O, memory allocation, thread synchronization. Your elegant business logic is rarely the villain. Wrap every outbound HTTP request with fine-grained timing, status codes, and the exact bytes sent and received. Log every filesystem operation that drags past a threshold. For GC’d languages, export pause histograms. The goal is to capture the system’s behavior right when it fails, not to log every internal state. High-cardinality logging of internal variables will bury you in noise and burn through your storage budget. Instead, log the inputs and outputs of critical sections. When a request fails, you want to replay those exact inputs against the same code path and see if it fails again. If it doesn’t, you’ve just proved the problem is environmental—not in your logic.

3. Time Is a Dimension, Not a Metric

Intermittent failures often boil down to race conditions, timeouts, or clock skew. Standard p99 latency hides the tail of the distribution. You need histograms. Switch your metrics library to emit latency as a histogram with buckets fine enough to catch microsecond-level lock contention. Plot heatmaps of request latency against time of day. Look for vertical bands of high latency that line up with log rotations, backup jobs, or Kubernetes autoscaling events. Check NTP sync across your fleet. A 50-millisecond clock drift between two services can turn a valid token into an expired one—but only when the drift aligns with the token’s issuance time. That’s an intermittent failure no code review will ever spot.

Digital oscilloscope display showing a complex waveform, representing the need to analyze timing and signal patterns in debugging

4. The Hypothesis-Driven Binary Search

Don’t guess. Form a falsifiable hypothesis and design an experiment to break it. “The failure is caused by a slow database query under high connection pool pressure.” Good. Now prove it wrong. Artificially saturate the connection pool in a staging environment and replay production traffic. If the failure doesn’t reproduce, your hypothesis is probably dead. Move on. This is a binary search over the system’s state space. Common hypotheses to test: Resource exhaustion (file descriptors, ephemeral ports, memory), concurrency bugs (non-atomic read-modify-write operations), garbage collection (stop-the-world pauses exceeding client timeouts), network packet loss (TCP retransmissions causing latency spikes), and data-dependent timing (a specific input size triggers an O(n²) algorithm that only times out under load). For each, define a clear metric that would spike if the hypothesis were true, and check historical data. If the metric doesn’t exist, add it and wait for the next occurrence.

5. The Art of the Targeted Canary

If the failure is rare—say, 0.01% of requests—you can’t just wait for it. You need to amplify the signal. Deploy a canary instance with exaggerated conditions: slash timeouts by a factor of 10, cap the connection pool at 1, or disable retries. This turns a rare race condition into a frequent failure. The canary will be useless for real traffic, but that’s not the point. It’s a scientific instrument. Route a copy of production traffic to it and watch the error logs. Once you can reproduce the failure reliably, you can bisect the code or configuration changes that fix it. Just remember to kill the canary after—nobody wants a crippled instance accidentally serving real users.

6. Read the Source, Not the Docs

Libraries lie. Their documentation describes ideal behavior. Their source code reveals default timeouts, retry logic, and error handling that can mask or mutate failures. An HTTP client might retry on 5xx errors by default, turning a transient upstream blip into a doubled request rate that crushes the upstream entirely. A connection pool might silently discard a connection after a TCP reset, then hand your application a fresh one, making you think the connection was persistent. Trace the failure path through the source. If you’re using a managed service and can’t read the source, treat it as a black box and instrument both sides of the API call. Compare what you sent to what the service received. The discrepancy is where the bug lives.

7. The Log Is the Last Resort

Structured logging is essential, but it’s a trailing indicator. By the time you’re grepping logs, you’ve already missed the chance to observe the system in its failing state. Logs should confirm a hypothesis, not generate one. When you do need them, ensure they include a unique request ID propagated across all services, the exact timestamp with microsecond precision, and the host/container ID. Without these, correlating an intermittent failure across a distributed system is a waste of time. If your logging system samples or drops lines under load, you’re flying blind. Fix that before the next incident.

8. Reproduce the Production Shape

Staging environments are sterile. They lack the chaotic traffic patterns, cache churn, and resource contention of production. To reproduce an intermittent failure, you need to mirror production’s shape: the distribution of request sizes, the mix of fast and slow endpoints, the diurnal traffic pattern. Use load-testing tools to replay a day’s worth of production traffic compressed into an hour. Run it against a full-scale clone of your production topology, not a minified version. If the failure only appears under sustained load, you need to sustain that load for hours. Yes, this is expensive. It’s less expensive than a 4-hour outage during Black Friday.

A person working late at night on multiple monitors displaying code and system dashboards, illustrating the intense focus required for debugging

9. Check the Kernel and Hardware

Application developers treat the OS as a given. That’s a mistake. Intermittent failures can originate in the kernel’s OOM killer, TCP stack tuning, or disk I/O scheduler. Check dmesg for OOM events, segmentation faults, or NIC ring buffer overflows. Look at SMART data for disks showing reallocated sectors—a single slow read can cascade into application timeouts. If you’re in the cloud, the hypervisor’s “noisy neighbor” problem can cause CPU steal time that manifests as random latency spikes. Plot steal time alongside your application latency. If they correlate, migrate to a different instance type or to dedicated hardware. This isn’t a software bug; it’s a capacity planning failure.

10. The Postmortem That Actually Matters

Once you’ve fixed the immediate issue, the real work begins. A postmortem that says “fixed a race condition in the connection pool” is useless. The postmortem should answer: Why did our observability fail to detect this? Why did our testing not catch it? What systemic change prevents this entire class of failure from recurring? For example, if a race condition caused the outage, the fix isn’t just the mutex you added. The fix is a static analysis rule that detects unprotected shared state, a load test that specifically exercises concurrent access, and a dashboard that alerts on lock contention. Write the postmortem as if you’re explaining to a future engineer who will face the same class of problem in a different service. Give them the tools to catch it before it catches them.

FAQ

Why do intermittent failures often disappear when I try to debug them?

Because the act of debugging changes the system’s timing. Attaching a debugger, enabling verbose logging, or simply SSHing into a box can alter CPU scheduling, memory pressure, or I/O patterns. This is known as the observer effect. The failure was dependent on a specific interleaving of events that your debugging tools disrupt. This is why you must rely on passive observation—metrics, distributed traces, and kernel-level event collectors—rather than interactive debugging.

How do I convince management to invest in reproducing production issues?

Stop calling it “reproducing issues.” Call it “chaos engineering” or “resilience testing.” Frame the cost of the reproduction environment against the cost of the last outage. If a 1% failure rate in a payment system costs $10,000 per hour, a $50,000 staging cluster that prevents a recurrence pays for itself in 5 hours. Present the business case, not the technical desire. If they still refuse, document the risk formally. When the next outage happens, your email is Exhibit A.

What’s the single most useful metric for catching intermittent failures?

Tail latency at the 99.9th percentile, broken down by service and endpoint. A spike in p99.9 latency almost always precedes a spike in errors. It’s the canary in the coal mine. If your monitoring system can’t calculate and alert on p99.9 latency in real time, you’re reacting to failures after users have already noticed. Fix that first.

How do I debug a failure that only happens once a month?

You don’t. You build a system that survives it. If a failure is that rare, the cost of debugging it likely exceeds the cost of the failure itself. Instead, focus on blast-radius reduction and automatic recovery. Implement circuit breakers, retries with exponential backoff, and graceful degradation. Ensure that when the failure occurs, the system isolates the damage and self-heals before a human is even paged. Then, log enough context to debug it post-hoc if the frequency increases. Sometimes, the best fix is accepting that 99.99% uptime is good enough.

Debugging Intermittent Failures in Production: A Field Guide for Engineers Who Hate Guesswork

The Unwelcome Surprise in Your Logs

You push a service on Tuesday. All tests pass. Dashboards glow green. Then, at 3:14 AM on Saturday, your phone screams—a payment endpoint just returned 500. By the time you’re awake and fumbling for your laptop, the error vanishes. The logs show a timeout, but the downstream service looks pristine. You squint, close the incident, and mumble something about a cosmic ray. It wasn’t a cosmic ray. Intermittent failures are the gremlins of production systems: they mock determinism, thrive in race conditions, and feast on resource exhaustion. This guide skips the fluff. It’s a technical, no-nonsense walkthrough for catching these ghosts, cutting them open, and making sure they never drag you out of bed again.

Server rack with blinking lights, representing production infrastructure

Nail the Failure Signature Before You Touch a Line of Code

Most engineers lunge for the codebase or start restarting pods. That’s a trap. Intermittent failures are statistical beasts. You need to map their shape: frequency, duration, which endpoints get hit, and what external events line up. Start with these questions:

  • Is it periodic? Check if the blip syncs with cron jobs, cache flushes, or traffic spikes. A service that keels over every hour on the hour is probably slamming into a rate limit or draining a connection pool tied to a scheduled task.
  • What’s the blast radius? Does it torch a single pod, an entire availability zone, or every instance? Use distributed tracing to see if errors cluster on specific nodes.
  • What changed recently? Even a tiny config tweak can shift timing. Bumping a connection timeout from 2 seconds to 200ms can turn a rare hiccup into a steady stream of 500s.

Pull data from your observability stack. If you don’t have one, you’re already flying blind. At a minimum, you need structured logs with trace IDs, metrics on request latency and error rates, and a way to query them across services. Without that, you’re just swapping campfire stories.

Instrument the Suspect Code Path

Once you’ve got a hunch—say, “the checkout service times out calling the inventory API”—add targeted instrumentation. Don’t scatter print statements like confetti. Use a library like OpenTelemetry to wrap every external call, database query, and lock acquisition in a span. The aim is to freeze the exact state when the failure bites.

For a Go service, you might wrap the HTTP client with a custom transport that logs request duration, status code, and any connection errors. In Python, slap a decorator on the suspect function that records arguments and return values on exception. The trick is to log before the error handler swallows the context. Too many systems spit out “Internal Server Error” with no stack trace because someone caught the exception and returned a generic blob.

Example: Snagging a Race Condition in a Database Update

Picture an order processing system that occasionally double-charges a customer. The code looks fine: it checks the order status before charging. But under high concurrency, two threads read “pending” at the same instant, then both charge. To catch this, log the order ID, thread ID, and the status read at the transaction’s start. Also log the database’s actual row version or timestamp. When the failure hits, you’ll see two threads with identical initial status and overlapping timestamps. The fix? A pessimistic lock or a conditional update using the row version.

Close-up of a network cable plugged into a server, symbolizing connectivity issues

Reproduce the Failure Without Prod Traffic

You can’t debug a Heisenbug by staring at dashboards. You need to trigger it on command. This is where teams often throw up their hands because “it only happens in prod.” That’s a lazy cop-out. Production has specific traits: real user traffic patterns, data volumes, network latency, and resource caps. You can simulate most of them.

  • Traffic replay: Use a tool like GoReplay to capture and replay production traffic against a staging environment. Crank the replay volume until the failure surfaces.
  • Chaos engineering: Inject network delays, packet loss, or CPU starvation into a canary instance. If the error rate spikes, you’ve found a resilience gap.
  • Shadowing: Mirror a slice of production requests to a new code path that has extra logging. Compare responses between the old and new paths to spot anomalies.

If the failure is tied to a specific data shape—like a user with a monstrous shopping cart—extract that data from production (sanitized) and feed it into a load test. The point is to turn an unpredictable event into a repeatable experiment.

Dissect the Network Layer Like a Surgeon

Plenty of intermittent failures are network gremlins, but developers blame the application because they don’t understand the transport. TCP retransmissions, DNS timeouts, and load balancer health checks can all cause sporadic errors. Learn to read a packet capture. Tools like tcpdump and Wireshark aren’t just for the network team.

Look for:

  • SYN retransmissions: If the client fires multiple SYN packets before a connection establishes, the server might be overloaded or the network is dropping packets. This causes variable latency and occasional “connection refused” errors.
  • DNS failures: A cached DNS record that expires mid-request can spike resolution time. Check your application’s DNS cache settings and the TTL of your service records.
  • Load balancer resets: If the load balancer kills idle connections before the application’s keep-alive timeout, you’ll see “connection reset by peer” errors. A classic mismatch that only shows up under low traffic.

One team I worked with burned two weeks debugging a 0.1% error rate on a gRPC service. The culprit: the load balancer’s idle timeout was 60 seconds, but the gRPC client’s keep-alive was 90 seconds. During quiet spells, the balancer axed the connection, and the next request failed. The fix was a one-line config change.

Correlate Events Across Distributed Systems

In a microservices mess, a failure in Service A might start from a timeout in Service B, which was triggered by a garbage collection pause in Service C. Without correlation, you’ll waste hours staring at the wrong service. Distributed tracing is non-negotiable. Use a system like Jaeger or Zipkin to propagate trace context through all services. When an intermittent failure hits, grab the trace and examine every span.

Pay attention to:

  • Span duration anomalies: A span that normally takes 10ms but occasionally balloons to 2s. Drill into the logs for that specific span to see if there’s a slow query, a lock wait, or thread pool exhaustion.
  • Missing spans: If a trace is incomplete, the service might have dropped the request due to a full queue. Check the service’s thread pool metrics and rejected execution count.
  • Error propagation: A downstream 500 might get wrapped as a 503 by an upstream service. The trace will show the original error, but your alerting might only see the 503. Always trace back to the root cause.

Engineer analyzing server logs on multiple monitors

Check Resource Limits and Kernel Behavior

Applications don’t run in a vacuum. The OS enforces limits that can trigger intermittent failures under load. Common culprits:

  • File descriptor exhaustion: Every socket, open file, and pipe eats a file descriptor. If your process hits the ulimit, it’ll fail to accept new connections or open files. The error often says “Too many open files,” but it might show up as a cryptic “Connection refused” if the accept loop dies.
  • Ephemeral port exhaustion: On Linux, the default ephemeral port range is about 28,000. If your service makes tons of outbound connections and doesn’t reuse them, you’ll run out of ports. The symptom: connect() fails with EADDRNOTAVAIL, but only under high connection churn.
  • TCP TIME-WAIT accumulation: After closing a connection, the socket sits in TIME-WAIT for 60 seconds. If you open and close thousands of connections per second, you can exhaust available ports. Enable tcp_tw_reuse and consider connection pooling.

Monitor these kernel-level metrics with tools like ss -s, netstat, or Prometheus node exporter. Set alerts on file descriptor usage and port exhaustion. These are leading indicators of intermittent failures.

Test Your Error Handling Under Realistic Conditions

Most error handling code never gets tested. Developers write a try-catch block, log the error, and move on. But in production, that catch block might get called with a half-open socket, a null pointer, or a corrupted response body. The error handler itself can cause a secondary failure that masks the original problem.

To fix this, inject faults at the integration points. Use a library like Toxiproxy to simulate network timeouts, connection resets, and slow responses. Write integration tests that verify the system’s behavior when the database returns a partial result or the cache sends a malformed response. The goal is to make your error paths as battle-tested as your happy paths.

One common anti-pattern: a retry loop without exponential backoff. When a downstream service slows down, the retries amplify the load and cause a complete outage. Always use jittered exponential backoff and a maximum retry limit. And never retry on a 400-level error—that’s a client mistake, not a transient fault.

Use Feature Flags for Safe Experimentation

When you think you’ve found the fix, don’t just deploy it to production and pray. Use a feature flag to enable the fix for a small percentage of users or requests. Compare the error rate between the flagged and non-flagged groups. If the fix works, gradually ramp it up. If it doesn’t, kill the flag instantly without a rollback.

This approach also helps with diagnosis. You can add a flag that enables extra logging or a different timeout value for a subset of traffic. This lets you gather more data without impacting all users. Just make sure your flagging system is fast and reliable—a slow flag evaluation can add latency and cause its own intermittent failures.

Document the Failure and the Fix

Intermittent failures have a nasty habit of recurring. Someone will refactor the code six months later and reintroduce the same race condition. Write a postmortem that includes:

  • The exact symptoms and how to detect them.
  • The root cause, with evidence (logs, traces, packet captures).
  • The fix, with a link to the code change.
  • Any new monitoring or alerts added to catch the failure early.

Store this in a shared knowledge base, not a siloed Google Doc. When the on-call engineer gets paged at 3 AM, they should be able to search for the error message and find your postmortem. This isn’t bureaucracy; it’s self-defense.

FAQ

Why do intermittent failures often happen during low-traffic periods?

Low traffic can expose resource reclamation issues. Connection pools may shrink, caches may expire, and idle timeouts may fire. When traffic resumes, the first few requests pay the cost of re-establishing connections or repopulating caches, causing timeouts. Also, garbage collection in managed runtimes can be more aggressive during idle periods, leading to stop-the-world pauses that delay request processing.

How can I tell if an intermittent failure is caused by a race condition or a resource leak?

Race conditions typically produce errors that are tightly correlated with concurrent requests—look for overlapping timestamps and shared mutable state. Resource leaks (memory, file descriptors, threads) cause a gradual degradation over time, with failures becoming more frequent until the process is restarted. Monitor resource usage trends; a sawtooth pattern that resets on restart points to a leak.

What’s the fastest way to get actionable data when an intermittent failure is happening right now?

If the failure is active, take a thread dump (for JVM languages), a heap profile, or a goroutine dump (for Go). These snapshots show exactly what every thread is doing at that moment. If you see many threads blocked on the same lock or waiting for a network response, you’ve found your bottleneck. Also, increase the log level for the suspect service temporarily—but be ready to revert it to avoid drowning in noise.

Can intermittent failures be caused by the monitoring system itself?

Yes. Health checks that are too frequent can overload a service. Metrics collection that allocates memory can trigger garbage collection. Even log aggregation can consume enough CPU to cause request timeouts. Always profile your observability overhead and ensure it’s a small fraction of total resource usage. If you suspect the observer effect, disable monitoring briefly on a canary instance and see if the failure rate changes.

When Your System Breaks Only Sometimes: A Field Guide to Intermittent Production Failures

There’s a special kind of frustration that comes with a bug that only shows up once in a blue moon. Not enough to set off every alarm, but just enough to make you dread checking Slack at 2 a.m. The real cost isn’t the bug itself—it’s the hours your team burns trying to corner something that refuses to be cornered. You stare at dashboards. You replay the same log lines. You mumble “works on my machine” and blame the network. Stop. If the failure happens once every ten thousand requests, it’s not random. It’s just waiting for a very specific set of conditions. Let’s talk about how to hunt it down.

Server rack with blinking lights

“Intermittent” Is Just a Fancy Word for “I Haven’t Found the Trigger Yet”

Calling a failure intermittent is a confession, not a diagnosis. Somewhere in your stack, a precise combination of state, timing, and input is lining up just right to break things. Maybe it’s a race condition that only fires when two requests hit the same record within a 50-millisecond window. Maybe a garbage collection pause runs long because your heap is 87% full and a promotion fails. Maybe a load balancer health check and a real request collide on a thread pool that’s one thread short. The conditions are rare, but they’re repeatable. Your job is to shrink the haystack until the needle is obvious.

First, get specific. “The checkout endpoint throws a 500 sometimes” is a complaint, not a starting point. You need the exact error message, the full stack trace, the HTTP method, the endpoint path, the timestamp down to the millisecond, and any request payload that might have triggered it. If your logs don’t give you that, you’re flying blind. Fix the logging before you do anything else. You can’t debug a ghost.

Look at the Seams, Not Just the Fabric

Application code usually gets the logging love. The seams between systems? Not so much. Load balancer health checks, database connection pools, outbound HTTP calls, message queue consumers—these are where intermittent failures love to hide. A connection pool drains for 200 milliseconds and nobody notices until a request lands in that gap. A DNS lookup times out once because a resolver was restarting. A TLS handshake fails because an intermediate certificate expired, but only for clients that don’t cache the chain.

Structured logging at every boundary, with correlation IDs that survive across services. If a request dies, you should be able to follow its entire journey without stitching together timestamps by hand. Using distributed tracing? Check your sampling rate. A 1% sample is great for dashboards, but it’s a statistical guarantee you’ll miss the rare stuff. For debugging, crank it to 100% on the affected service. Yes, it’ll cost you in storage. Cheaper than another all-nighter.

Close-up of network cables and server indicators

Your Assumptions About the Stack Are Probably Wrong

Intermittent failures have a knack for exposing the lies we tell ourselves about how the stack works. I once lost three days to a file upload endpoint that failed 0.01% of the time. The error was a timeout, but only for files between 2.1 and 2.3 MB. The smoking gun: nginx had a default client_max_body_size of 2 MB. The backend accepted the connection anyway and then just… hung. The frontend load balancer had a 60-second timeout, the backend had 30, so the connection dangled until the client gave up. The fix took five minutes once we saw the interaction.

Here’s a checklist of assumptions to gut-check:

  • Timeouts: Map every timeout in the request path—load balancer, reverse proxy, app server, database client. A 30-second app timeout sitting behind a 29-second proxy timeout creates a one-second window where the proxy kills the connection and the app never knows.
  • Connection pooling: Are you leaking connections? A pool that slowly drains under a specific traffic shape will cause failures that vanish after a restart, only to creep back hours later.
  • Serialization: Does your code assume a JSON field is always present? A malformed payload from a buggy client—missing a nested field, sending a string instead of an int—can trigger a deserialization path your normal traffic never exercises.
  • Garbage collection: In managed runtimes, a full GC pause can outlast your health check timeout. The orchestrator marks the instance unhealthy, shifts traffic, and by the time you look, the instance is back to normal and you’re left scratching your head.

Hypothesis First, Data Second

Staring at logs and hoping a pattern jumps out is a recipe for wasted hours. Write down a specific guess about what’s happening, then design a query or experiment to prove it wrong. Suspect a race condition? Look for requests that overlap in time and touch the same resource—same user ID, same database row, same file path. Suspect resource exhaustion? Correlate failure timestamps with CPU steal, memory pressure, file descriptor counts, or thread pool saturation. Suspect a specific client version? Filter by user agent and see if the failure rate clusters.

Real example: a payment service kept throwing “duplicate transaction” errors, but only occasionally. Hypothesis: two requests with the same idempotency key arriving within milliseconds. Test: grep logs for idempotency keys that appeared more than once in a 100ms window. Result: a mobile client was firing a retry on a background thread without cancelling the original request. Both threads used the same key. The fix wasn’t on the server at all.

Reproduce It in Production (No, Really)

“Can’t reproduce in staging” is the most expensive phrase in operations. Staging doesn’t have production traffic, production data shapes, or production’s weird network topology. If you can’t trigger the bug in staging, take the fight to production—but do it safely. Route a fraction of traffic to an over-instrumented instance. Use internal-only debug endpoints that dump the full request context. Send synthetic requests that mimic the failing pattern, with headers and payloads copied from real failures.

One trick I lean on: deploy a canary with verbose diagnostics, then route only the suspicious traffic to it. If the failure correlates with users in a specific region or requests with a certain content type, isolate that slice. The canary will fail, but you’ll capture the entire state at the moment of failure—heap dumps, thread stacks, connection pool status, the works. Then kill the canary and go read the tea leaves.

Engineer analyzing server logs on multiple monitors

Sometimes It’s the Hardware (Even in the Cloud)

The cloud abstracts hardware, but it doesn’t make hardware problems disappear. A noisy neighbor on a shared host steals CPU cycles, and you see it as intermittent latency spikes. A flaky network interface on a physical machine drops packets, and your retry logic masks it until it doesn’t. ECC memory errors are rare, but they happen. If you’ve ruled out every software cause, it’s time to look at the metal.

Check your cloud provider’s instance health metrics. Look for hypervisor events, live migrations, or hardware maintenance notifications that line up with your failure timestamps. On bare metal, dmesg is your friend—search for MCE (Machine Check Exception) errors. I once saw a team trace a 0.001% failure rate to a single rack where a top-of-rack switch was flapping a port. The switch logs had the evidence the whole time. Nobody had looked.

FAQ

Why do intermittent failures tend to get worse over time?

Because the underlying condition usually gets more frequent. A slow memory leak eventually fills the heap, GC pauses stretch out, and timeouts start piling up. A connection pool leak drains available connections until every request fails. A database table growing without proper indexes makes queries slower and slower until they slam into a timeout wall. The failure rate climbs as the system drifts toward a cliff.

How do I get management to approve more production logging?

Show them the bill for not doing it. Tally the engineering hours already burned, the customer complaints, the risk of a full-blown outage if the root cause stays hidden. Propose a temporary log verbosity bump with a hard expiration date—say, one week. Most managers will greenlight a week of noisy logs over a weekend of emergency pages.

What’s the first thing to check when an intermittent failure pops up?

What changed. Deployment history, config tweaks, dependency updates, infrastructure modifications. Even a tiny change—a new connection pool setting, a library patch version bump, a firewall rule adjustment—can introduce failures that only bite under specific conditions. If you have a change log, correlate failure timestamps with change timestamps. If you don’t have a change log, start one right now.

Can monitoring tools actually cause intermittent failures?

Absolutely. Health check endpoints that do real work—querying a database, calling an external service—add load that can tip a system over the edge. Monitoring agents that collect too many metrics can eat CPU and memory, causing resource contention. Even log frameworks can block when their buffer fills up, introducing latency that cascades into timeouts. Your observability stack is part of the system; treat it as a suspect.

How do I deal with intermittent failures caused by third-party services?

You can’t fix their code, but you can stop their failures from torching your system. Retries with exponential backoff and jitter. Circuit breakers that stop calling a failing service before the backlog takes down your own threads. Fallbacks—stale caches, default responses, graceful degradation. And instrument every external call so you can hand their support team a timestamp, a request ID, and a stack trace instead of a vague “your API is slow sometimes.”

Debugging Intermittent Failures in Production: A No-Nonsense Guide

The Unforgiving Nature of Transient Bugs

You push a deploy late Friday, glance at the dashboards, and everything hums along green. Monday morning, your phone lights up with alerts. A payment gateway choked for three users. A database query timed out twice. By the time you crack open your laptop, the errors have vanished. This is the reality of intermittent failures in production systems. They aren’t the tidy, reproducible crashes you can corner in a debugger. They’re ghosts in the wiring, set off by race conditions, resource exhaustion, or network hiccups that refuse to show themselves on command.

Plenty of engineers write these off as one-off gremlins. Restart a service, shrug, and move on. That’s a mistake. An intermittent failure is a crack in your system’s foundation. It will widen under load, during peak traffic, or at 3 AM when you’re dead asleep. The only way to fix it is to hunt it down with a methodical, evidence-based approach. No guesswork. No superstition. Just hard data and a clear chain of causality.

Server rack with blinking lights indicating activity

Start with the Symptom, Not the Solution

An alert fires for a 504 Gateway Timeout that clears itself. Your first thought is to blame the network. Don’t. The network is a lazy scapegoat. Treat the symptom like a crime scene. What actually failed? A specific API endpoint? A database connection? A message queue consumer? Narrow the blast radius. If it’s a timeout, was it the client side or the server side that gave up? If it’s a 500, where’s the exact stack trace? If the stack trace is missing, you’ve got a logging gap that needs plugging before you can even start.

Intermittent failures often leave breadcrumbs in metrics, not logs. A spike in p99 latency that lines up with the error tells you more than a generic exception. Dig into your time-series data. Did the failure coincide with a garbage collection pause? A sudden drop in available database connections? A burst of requests from a single IP range? The symptom isn’t the error message itself. It’s the deviation from normal operating parameters. Define normal first, then spot the deviation.

Instrument Before You Investigate

If your system lacks granular metrics, you’re flying blind. You need histograms of request durations, counters for every non-200 status code, and gauges for thread pool saturation. Without these, you’re relying on luck. Add structured logging with correlation IDs that cross service boundaries. A single request should be traceable from the load balancer right down to the database query. If you can’t do that, you’ve got architectural debt that makes debugging intermittent failures nearly impossible.

Once you’re instrumented, set up canary deployments or traffic shadowing to reproduce the failure safely. Never experiment in production if you can avoid it. Mirror a slice of live traffic to a staging environment that mimics production’s data shape and concurrency. The goal is to trigger the failure under controlled observation. If you can’t reproduce it, you can’t prove you fixed it.

Close-up of network cables and server indicators

Common Culprits and Their Signatures

Intermittent failures fall into predictable buckets. Recognizing the pattern speeds up diagnosis. Here are the usual suspects.

1. Resource Starvation Under Load

Your service handles 100 requests per second without a sweat but crumbles at 500. The failure isn’t consistent because load fluctuates. Look for connection pool exhaustion. A pool of 20 database connections works fine until a slow query holds connections longer than expected. New requests queue up and time out. The fix isn’t always a bigger pool; that can hammer the database harder. Instead, add circuit breakers, set query timeouts, and optimize that slow query.

Thread pool starvation follows a similar pattern. If all worker threads are blocked waiting on a downstream service, no threads remain to handle health checks or new requests. Use non-blocking I/O or separate thread pools for different request classes. Monitor thread pool queue depth. A growing queue is a leading indicator of imminent failure.

2. Race Conditions and State Corruption

Two requests update the same database row at the same time. One reads stale data, computes a new value, and writes it back, overwriting the other’s update. The error only happens when timing lines up perfectly. These are maddening because they leave no trace except incorrect data. Use optimistic locking with version numbers. When a write fails due to a version mismatch, retry with fresh data. Log every retry so you can measure contention frequency.

In distributed systems, race conditions emerge from out-of-order message delivery. A delete event arrives before the create event. The system rejects the delete because the entity doesn’t exist yet. Design for idempotency. Every operation should be safe to apply multiple times. Use event sourcing or a message broker that preserves order within a partition.

3. Time-Dependent Logic

Code that behaves differently at midnight, on the last day of the month, or during daylight saving transitions is a time bomb. A cron job that assumes 24 hours in a day fails on the day clocks spring forward. A cache TTL that expires exactly when a batch job runs causes a thundering herd. Audit every place your code touches the system clock. Replace relative time calculations with absolute timestamps where possible. Test by warping the system clock in a staging environment.

4. Network Partitions and Retry Storms

A brief network blip causes a few requests to fail. Your retry logic kicks in, multiplying the load on an already stressed service. The retries themselves time out, triggering more retries. This positive feedback loop can take down an entire cluster. Implement exponential backoff with jitter. Cap the number of retries. Use a circuit breaker that stops calling a failing service entirely for a short period. Monitor retry rates; a sudden increase signals a downstream problem.

Engineer analyzing server logs on multiple monitors

Building a Reproducible Test Case

You can’t fix what you can’t reproduce. But reproducing an intermittent failure takes creativity. Start by isolating the component. If the failure involves a database query, extract that query and run it under load with a tool like pgbench or sysbench. Vary the parameters. Introduce artificial delays. If the failure involves a network call, use a proxy like Toxiproxy to inject latency, packet loss, or connection resets. Chaos engineering isn’t just for Netflix; it’s a debugging technique.

For concurrency bugs, write a stress test that runs hundreds of goroutines or threads hammering the same code path. Use a race detector. For timing bugs, manipulate the system clock in a container. The key is to amplify the conditions that trigger the failure. If the bug appears once per 10,000 requests, run 100,000 requests in a loop. If it appears under high database load, simulate that load with a benchmark tool. The failure is deterministic given the right conditions; your job is to find those conditions.

Logging That Actually Helps

Most production logs are useless for intermittent failures. They’re stuffed with INFO-level noise and lack context. When a request fails, you need to see the entire lifecycle: incoming parameters, downstream calls with their latencies, database queries with bind parameters, and the exact response. Structured logging in JSON format lets you query logs like a database. Include a trace ID, span ID, and a sampled flag. Use debug-level logs that you can dynamically enable for a percentage of traffic without restarting the service.

Don’t log personally identifiable information or secrets. But do log business identifiers like user IDs and order IDs. When a user reports a problem, you can pull every log line for their session. That turns an intermittent ghost into a concrete sequence of events.

Post-Mortem Without Blame

Once you identify the root cause, document it. A good post-mortem isn’t a confession. It’s a technical analysis that prevents recurrence. Describe the symptom, the timeline of events, the root cause, the fix, and the detection gap. Why didn’t your monitoring catch this sooner? What metric or alert would have shortened the time to detection? Add that alert now. If the fix is a code change, write a regression test that simulates the exact failure condition. If the test can’t be automated, document a manual runbook for the on-call engineer.

Share the post-mortem with the team. Not to assign blame, but to spread knowledge. Intermittent failures are often systemic. The same race condition may exist in three other services. The same connection pool misconfiguration may be lurking in every microservice. Use the incident as a catalyst for a broader fix.

FAQ

Why do intermittent failures often happen under low load?

Low load can expose resource leaks that are masked under high throughput. For example, a connection pool that slowly leaks connections will eventually exhaust under sustained low traffic, but high traffic may recycle connections fast enough to hide the leak. Also, cron jobs or periodic tasks that run during quiet hours can trigger failures that go unnoticed until the next business day.

How do I debug a failure that only happens once a month?

First, ensure your logs and metrics have enough retention to cover the interval. If logs rotate after a week, you’ll never catch a monthly bug. Set up long-term storage for error logs and key metrics. When the failure occurs, preserve the evidence immediately. Then, look for patterns in the timestamp: day of week, phase of the moon (seriously, some billing systems run on lunar cycles), or correlation with external events like certificate expirations or third-party API maintenance windows.

Should I add a retry to fix an intermittent failure?

Retries mask the symptom; they don’t cure the disease. A retry is a bandage that stops the bleeding but leaves the wound infected. Use retries only after you understand the root cause and have determined that the failure is transient by design (e.g., a network glitch). Even then, retries must be idempotent, bounded, and paired with circuit breakers. Blind retries amplify load and can turn a minor hiccup into a cascading failure.

What’s the first thing to check when an intermittent failure appears?

Check your time-series metrics for any correlation with the failure window. Look at CPU, memory, garbage collection pauses, thread pool utilization, connection pool wait times, and downstream service latency. A sudden spike in any of these is a stronger clue than the error message itself. Also, check your deployment log. Did a config change or feature flag toggle coincide with the first occurrence? Many “intermittent” failures are actually deterministic consequences of a recent change that only manifest under specific conditions.

The Unforgiving Logic of Intermittent Failures: A Field Guide for Engineers Who Hate Guesswork

Intermittent failures are the cockroaches of production systems. They scatter when you turn on the lights, survive on the tiniest crumbs of resource contention, and breed in the dark corners of your infrastructure where monitoring daemons fear to tread. Most debugging guides treat them like mysteries to be solved with patience and luck. That approach is garbage. Intermittent failures are deterministic events whose trigger conditions you simply haven’t measured yet. The gap isn’t in your understanding of the bug—it’s in your instrumentation. This article lays out a method for closing that gap, written for engineers who’d rather amputate a limb than stare at a log file hoping for a pattern to emerge.

Close-up of tangled network cables in a server rack

Stop Calling It a Heisenbug

The term “Heisenbug” is a crutch. It implies the failure is inherently slippery, that observing it changes its behavior. In production systems, what actually changes is the state you’re observing, not the bug itself. A memory leak that only crashes the process under peak traffic isn’t a quantum event; it’s a threshold violation you haven’t mapped. A race condition that fires once every ten thousand requests isn’t capricious; it’s a probability distribution you haven’t charted. The first step in debugging intermittent failures is to ditch the vocabulary that treats them as supernatural. They’re ordinary bugs with narrow preconditions. Your job is to widen the aperture of your instrumentation until those preconditions become visible.

Start by defining the failure as a state machine. What’s the exact symptom? A 503 error? A dropped message? A corrupted record? Write down the precise observable output. Then list every component that participates in the request path: load balancers, application servers, databases, caches, queues, external APIs. For each component, identify the state variables that could influence the output. CPU utilization, memory pressure, connection pool saturation, garbage collection pauses, lock contention, clock skew, DNS resolution time. If you can’t list at least twenty variables, you haven’t thought hard enough.

Instrument the Edges, Not the Middle

Most teams instrument the happy path. They measure average response time, throughput, error rate. Intermittent failures live at the edges of your system’s operating envelope, so you need instrumentation that captures those edges. Add histograms, not averages. A p99.9 latency spike that lasts three seconds will be invisible in a mean, but it will correlate perfectly with a timeout failure. Emit metrics on queue depths, thread pool utilization, and file descriptor counts at the moment of each request. The goal is to turn every request into a rich telemetry trace that you can query later, not a single log line that says “Request failed.”

Structured logging is non-negotiable. Every log entry must include a correlation ID, the exact timestamp with millisecond precision, and a dictionary of context: hostname, container ID, request parameters, upstream service latencies. When a failure occurs, you should be able to pull the full trace and see the state of every dependency at that instant. If your logging library can’t do this, replace it. If your ops team complains about storage cost, remind them that the cost of an unsolved intermittent failure is measured in customer trust and engineering burnout.

Engineer staring at multiple monitors displaying graphs and logs

Correlation Harvesting: The Poor Man’s Distributed Tracing

You don’t need a fancy distributed tracing platform to start finding correlations. You need a script that queries your log aggregator and your time-series database simultaneously. For every failed request ID, pull the metrics from the surrounding sixty-second window. Dump them into a CSV. Then do the same for a sample of successful requests. Run a simple statistical test—Mann-Whitney U works fine for non-normal distributions—on each metric. The metrics with the largest effect size between the failure group and the success group are your prime suspects. This isn’t machine learning; it’s basic exploratory data analysis that any competent engineer can do in an afternoon with Python and pandas.

I once tracked down a database connection timeout that occurred roughly every four hours. The mean connection acquisition time was 2 ms. The p99 was 15 ms. The failure threshold was 30 ms. By pulling connection pool metrics at the time of each timeout, I found that the pool’s “active connections” count spiked to exactly the maximum pool size in the seconds before the failure. The root cause was a background job that ran every four hours and opened a new connection without using the pool, momentarily starving other consumers. The fix was one line of configuration. The investigation took two hours. The bug had existed for six months because nobody had correlated the pool metrics with the timeout events.

Reproduce by Amplifying the Stressors

You can’t wait for the failure to happen again. You have to force it. Identify the suspected preconditions from your correlation analysis and amplify them in a staging environment that mirrors production topology. If you suspect a race condition under high concurrency, don’t run a polite load test at 100 requests per second. Run at 10,000 requests per second with deliberate connection jitter. If you suspect a memory leak, deploy a canary instance with half the normal heap size and watch it crash faster. The goal is to shrink the mean time between failures from days to minutes. Once you can reproduce the failure on demand, you own it. You can attach a debugger, add temporary logging, and bisect the codebase until you find the exact line.

Chaos engineering is useful here, but most teams misuse it. They randomly kill pods and call it a day. That only tests your recovery mechanisms, not your root cause hypotheses. Instead, design chaos experiments that target your specific suspected preconditions. If you think the bug is triggered by a slow downstream service, inject latency into that service’s responses—not random latency, but a precise sawtooth pattern that lets you map the exact threshold where failures begin. If you think the bug is triggered by a specific sequence of requests, write a script that replays that sequence in a tight loop. The more surgical your experiment, the faster you’ll isolate the trigger.

Traffic Shadowing with a Difference

Sometimes you can’t reproduce the failure in staging because the production data shape is too complex. In those cases, use traffic shadowing—but don’t just mirror requests. Mirror them with mutated parameters that explore the edge cases. If the failure correlates with requests that have large payloads, shadow every request with a payload size multiplied by 1.5. If the failure correlates with a specific user agent, shadow requests with that user agent injected. Run the shadow traffic against a canary instance that has extra debug logging enabled. The goal is to create a parallel production-like stream that is more likely to hit the failure than real traffic, while still being safe to discard.

Time Is a Lie: Clock Skew and Partial Failures

Distributed systems make time hard. An intermittent failure that looks like a timeout might actually be a clock skew problem where a token was considered expired before it was issued. Always compare timestamps from different machines using a monotonic clock reference, not wall clock. If your system uses NTP, log the estimated offset at the time of each request. I’ve seen failures caused by an NTP server that stepped the clock backwards by two seconds during a leap second event, invalidating a whole batch of signed URLs. The failure was intermittent because it only affected requests that spanned the clock step. The fix was to configure the NTP daemon to slew, not step, and to add a grace period to token validation.

Partial failures are another time-related trap. A request that writes to a primary database but fails to update the cache is not a complete failure; it’s a state divergence that will cause incorrect reads later. The user who triggered the write sees a success. The user who reads the stale cache ten minutes later sees the failure. The two events are separated in time, so naive correlation by request ID will miss the link. You need to track causal chains: every write must log the keys it invalidates, and every read must log the cache state it observed. When a read returns stale data, you can walk backwards through the write log to find the invalidation that should have happened but didn’t.

Digital clock display with glitched segments

Kernel-Level Traps You’re Ignoring

Application engineers tend to blame the application. Sometimes the failure is beneath you, in the kernel or the hypervisor. Transparent hugepage compaction can stall a process for hundreds of milliseconds, causing timeouts that look like application hangs. Memory cgroup limits can trigger OOM kills that leave no application log entry except a sudden process death. Conntrack table overflow can drop packets silently, making it look like a downstream service is unreachable when the packets never left the machine. If your intermittent failure involves network timeouts or unexplained process deaths, spend an hour with dmesg, perf sched, and conntrack -S. The evidence is there, but your application monitoring will never see it.

One memorable failure involved a service that would hang for exactly 200 ms every few minutes. Application logs showed a gap with no activity. Strace revealed the process was blocked in a futex call. Perf tracing showed the kernel was doing transparent hugepage compaction on the same NUMA node. Disabling THP compaction made the hangs disappear. The application code was never at fault. The lesson: if your failure has a suspiciously round duration—100 ms, 200 ms, 1 s—look for kernel timers or hardware interrupts with the same period.

Build a Failure Resume

Every intermittent failure that you solve should leave behind a permanent artifact: a failure resume. This is a document—stored in the repository, not a wiki that will rot—that describes the symptom, the root cause, the method of detection, and the fix. Include the exact queries you ran, the metrics you correlated, and the experiment that reproduced the failure. The next time an intermittent failure appears, an engineer can scan the failure resumes and find a similar pattern in minutes instead of starting from scratch. This isn’t post-mortem bureaucracy; it’s a tactical asset. A good failure resume reads like a detective’s case notes, not a corporate memo.

Structure the resume with these sections: Symptom Signature (what the user or monitoring saw), Affected Components (the exact services and infrastructure), Trigger Conditions (the state variables that had to align), Detection Method (the queries and correlations that surfaced the cause), Reproduction Steps (how to force the failure in staging), and Fix (the code or configuration change). If you can’t fill out every section, the investigation isn’t complete.

FAQ

Why do intermittent failures often correlate with deployment events?

Deployments change multiple variables simultaneously: new code, restarted processes, reset connection pools, flushed caches. An intermittent failure that appears after a deployment is often caused by a cold start effect—caches are empty, JIT compilers haven’t warmed up, connection pools aren’t saturated. The failure disappears after a few hours because the system reaches steady state. To debug, compare the first hour of metrics after deployment with the same hour from the previous day. Look for elevated latencies, higher error rates, or different resource usage patterns. The fix is usually to add warm-up logic or to stagger the deployment so that not all instances restart at once.

How do you debug a failure that only happens in production and can’t be reproduced?

You don’t need to reproduce the exact failure to find the cause. You need to reproduce the conditions that lead to the failure. If the failure correlates with high memory usage, run a canary with artificially limited memory and see if the failure rate increases. If it correlates with a specific API call pattern, write a script that replays that pattern at high concurrency. The key is to isolate the suspected precondition and amplify it until the failure becomes frequent enough to study. If you can’t identify any preconditions, your monitoring is insufficient. Go back and add more granular metrics.

What’s the most overlooked source of intermittent failures in microservices?

Partial failures in service meshes. A request that succeeds at the application layer can still fail if the sidecar proxy has a stale endpoint list or a misconfigured retry policy. The application sees a 200 OK, but the proxy retried the request to a different instance, causing duplicate processing. Or the proxy timed out and returned a 504, but the upstream service actually processed the request, leading to an inconsistent state. Always compare application-level status codes with proxy-level status codes. Enable proxy logging at debug level for a sample of traffic. The failure you’re chasing might not be in your code at all.

How do you convince management to invest time in debugging intermittent failures?

Stop asking for permission. Intermittent failures are production incidents that happen to have a low frequency. Track their business impact: count the number of affected users, the revenue at risk, the support tickets generated. Present the data as a cumulative cost over the time the failure has existed. A failure that affects 0.1% of requests on a system handling 10 million requests per day is 10,000 failures per day. That’s not a minor annoyance; it’s a significant reliability gap. If management still resists, ask them which 10,000 daily users they’re willing to lose. The conversation usually shifts quickly.

How to Write Runbooks That Actually Get Used During Incidents

I wrote my first real runbook at 2:14 AM, sitting on a plastic chair in a Lagos server room, listening to a diesel generator cough outside. The payment switch had failed. Again. The previous engineer’s documentation was a single text file: “Restart service. If still down, call Felix.” I was Felix. The service was down. And the file told me nothing about why restarting might work, what state the restart would leave the ledger in, or how to verify that we hadn’t double-posted transactions. That night, I learned that most runbooks are not technical documents. They are theater. They exist to satisfy an audit checkbox, not to guide a human being through a system failure under duress.

The problem is not that engineers are lazy. The problem is that we write runbooks as if the reader is a calm, well-rested colleague with perfect recall of the system architecture. In reality, the reader is you, at 2 AM, on your third cup of Nescafé, with a generator that might run out of diesel in forty minutes. Your working memory is shot. Your hands are shaking slightly from caffeine and cortisol. You need a document that does not just list steps. You need a document that tells a story.

Why Most Runbooks Are Documentation Theater

Walk through any engineering team’s runbook repository and you will find the same pattern: a flat list of commands, a few shell snippets, and a vague instruction to “check the logs.” These documents fail for three reasons. First, they omit context. They tell you what to run but not why you are running it, which means you cannot adapt when the expected output does not appear. Second, they assume a single failure mode. Real incidents are branching narratives. The database is slow because the disk is full, or because a replication slot is stuck, or because someone ran a schema migration during peak traffic. A linear runbook collapses at the first fork. Third, they are never tested. The team writes them during a calm sprint, files them away, and discovers they are useless only when the pager fires.

This is not a new observation. The Google SRE book dedicates entire chapters to emergency response and managing incidents, emphasizing that effective troubleshooting requires structured, context-rich documentation. The SRE approach treats runbooks as living artifacts that reduce mean time to recovery by guiding engineers through decision trees and system dependencies. But most teams outside the hyperscaler bubble do not have the bandwidth to maintain that level of documentation rigor. We need a lighter-weight method that still captures the narrative structure of a real incident.

The Narrative Structure of a Useful Runbook

Think of a runbook as a script. Not a screenplay with character arcs, but a tight technical script with clear scenes, character motivations, and branching plotlines. Every incident has a beginning, a middle, and—if you are lucky—an end. Your runbook should mirror that structure.

Scene One: Symptoms. What does the failure look like from the outside? Be specific. “Payment switch returns HTTP 503” is better than “service is down.” Include the exact error message, the monitoring dashboard URL, and the alert that fired. The goal is to confirm within thirty seconds that the runbook matches the incident. If the engineer has to guess whether this is the right document, you have already lost five minutes of generator runtime.

Scene Two: System Dependencies. Every service has dependencies. Your runbook must name them explicitly: the PostgreSQL primary, the Redis instance, the upstream NPSB (Nigerian Payment Service Bureau) connection, the VSAT link to the backup site. For each dependency, state how to check its health and what the expected healthy state looks like. During that Lagos outage, I wasted twenty minutes chasing a payment switch bug that was actually a failed NPSB TLS handshake because the runbook never mentioned the external dependency.

Scene Three: Decision Tree. This is where most runbooks collapse into a wall of text. Instead, structure the troubleshooting as a series of binary questions. “Is the database reachable? If yes, go to step 4. If no, go to step 7.” Each branch should have a clear exit condition. You are not writing a novel. You are writing a choose-your-own-adventure book where the stakes are real money.

Scene Four: Recovery Actions. For each leaf node in the decision tree, provide the exact commands to run, the expected output, and the rollback procedure if the recovery action makes things worse. Include the command to check the current state before you run the recovery. I have seen engineers restart a database replica without checking if it was still catching up, turning a five-minute outage into a six-hour resync.

Scene Five: Verification. How do you know the system is actually healthy? “Service returns 200” is not enough. For a payment switch, verify that a test transaction settles end-to-end. For a database, verify that replication lag is below your threshold. State the exact query or API call, and state the expected result. If the verification step fails, the runbook should loop back to the decision tree.

Writing the Script: A Concrete Example

Let me ground this in a real scenario. You are running a payment ledger on a single ARM board with 512MB RAM, using SQLite as the primary store. The board is in a rack in Lagos, powered by a generator that has been running for six hours. The alert fires: “Ledger write latency > 5 seconds.” You SSH in over a flaky 4G connection. What does a narrative runbook look like?

Symptoms: Alert name: ledger_write_latency_high. Dashboard: Grafana panel “Ledger Write Latency p99” at http://monitor.lagos.internal:3000/d/ledger. Expected: p99 < 500ms. Current: p99 = 8.2s. Check: curl -s http://ledger.lagos.internal:8080/health | jq .write_latency_ms.

Dependencies: SQLite database at /data/ledger/ledger.db. Disk: /dev/sda1 (ext4, 64GB eMMC). Power: generator phase A, monitored via ups.lagos.internal. Network: MTN 4G router at 192.168.1.1.

Decision Tree:

  1. Is the disk full? Run df -h /data. If usage > 90%, go to Recovery A (disk cleanup). If not, go to step 2.
  2. Is SQLite experiencing a write lock? Run sqlite3 /data/ledger/ledger.db "PRAGMA busy_timeout;". If timeout is 0, go to Recovery B (set busy_timeout). If not, go to step 3.
  3. Is the eMMC throttling? Run cat /sys/class/thermal/thermal_zone0/temp. If > 80°C, go to Recovery C (thermal throttling). If not, escalate to on-call engineer.

Recovery A (Disk Cleanup): Check WAL size: ls -lh /data/ledger/ledger.db-wal. If > 100MB, run sqlite3 /data/ledger/ledger.db "PRAGMA wal_checkpoint(TRUNCATE);". Verify: df -h /data shows usage < 80%. Rollback: none; checkpoint is safe. If checkpoint fails, escalate.

Recovery B (Write Lock): Set busy_timeout: sqlite3 /data/ledger/ledger.db "PRAGMA busy_timeout=5000;". Verify: re-run health check, p99 < 500ms. Rollback: PRAGMA busy_timeout=0;.

Recovery C (Thermal Throttling): Check if fan is running: cat /sys/class/hwmon/hwmon0/fan1_input. If 0 RPM, manually start fan: echo 255 > /sys/class/hwmon/hwmon0/pwm1. Wait 5 minutes. Re-check temperature. If still > 80°C, reduce write load: systemctl stop ledger-reconciliation.timer. Verify: temperature < 70°C, p99 < 500ms. Rollback: systemctl start ledger-reconciliation.timer after temperature stabilizes.

Verification: Run end-to-end test: curl -X POST -d '{"amount": 100, "from": "test", "to": "test2"}' http://ledger.lagos.internal:8080/transact. Expect HTTP 201 and transaction ID. Check balance: curl http://ledger.lagos.internal:8080/balance/test2. Expect 100. If either fails, return to decision tree step 1.

This runbook is not beautiful. It is functional. It assumes the reader is tired, stressed, and working with limited tools. It does not require a separate wiki page to explain what a WAL checkpoint is. It embeds the context directly in the recovery step.

Why Narrative Structure Reduces MTTR

Cognitive load is the enemy of incident response. When you are on call, your brain is running a finite state machine with limited stack depth. A linear runbook forces you to hold the entire decision tree in your head, simulating branches mentally while also trying to remember which commands you have already run. A narrative runbook externalizes that state. Each step tells you where you are in the story, what you have learned so far, and what the next meaningful question is.

This approach aligns with the NIST Cybersecurity Framework, which treats incident response and recovery as structured functions requiring tested playbooks. NIST’s guidance emphasizes that operational procedures should be maintained and exercised, not filed away. A runbook that follows a narrative structure is inherently testable: you can walk through each branch during a tabletop exercise and discover where the script breaks.

During that Lagos outage, the generator failed at hour four. I had forty minutes of UPS runtime to either fix the payment switch or shut it down cleanly. The narrative runbook I wrote afterward—the one I just described—let me run through the decision tree in under ten minutes on a subsequent incident. The difference was not technical skill. It was that the document did not make me think about what to do next. It told me a story I had already rehearsed.

Maintaining the Script: Runbooks as First-Class Deliverables

A runbook is not a one-time artifact. It rots. Dependencies change, recovery commands drift, and the person who understood the original failure mode leaves the team. Treat runbooks like source code. Version them in the same repository as the service they document. Review them during code reviews. If a pull request changes the database schema, it must also update the runbook’s verification queries. If a new dependency is added, the runbook’s dependency section must reflect it.

I keep my runbooks in plain text, stored alongside the service configuration. No wikis, no Confluence pages that require VPN access when the VPN is down. A runbook.md file in the service repository, rendered by a simple static site generator, accessible over the local network even when the internet is unreachable. The format is deliberately constrained: Markdown with explicit section headers, no collapsible sections, no JavaScript. The document must be readable in less over an SSH session.

Writing a good runbook is closer to writing a technical script than writing documentation. You are crafting a sequence of scenes, each with a clear dramatic question: “Is the disk full?” “Is the database reachable?” “Is the thermal sensor lying?” The answer propels the reader to the next scene. This is why I find that tools designed for narrative structure—like a script writing app that forces you to think in scenes and beats—can clarify the mental model even for operational documents. You do not need Final Draft to write a runbook, but you do need the discipline of scene-based thinking. Every section must earn its place by answering a question the on-call engineer will actually ask.

The Decision Checklist: Is Your Runbook Useful or Theater?

Before you file a runbook as “done,” run it through this checklist. If you answer “no” to any question, the document is not ready for production.

  1. Can a tired engineer confirm in 30 seconds that this is the right runbook? The symptoms section must include the exact alert name, error message, or dashboard panel. If the engineer has to grep through a wiki search, the runbook has already failed.
  2. Does the runbook name every external dependency and how to check its health? If the payment switch depends on an NPSB connection, the runbook must include the command to test that TLS handshake. Do not assume the reader remembers.
  3. Is the troubleshooting structured as a decision tree with binary questions? “Check the logs” is not a decision. “Is the error log showing ‘connection refused’? If yes, go to step 4” is a decision.
  4. Does every recovery action include a pre-check command, the recovery command, expected output, and a rollback procedure? If you cannot roll back, state that explicitly and explain why.
  5. Does the verification step test the system end-to-end, not just the component you touched? Restarting the database is not a fix. A successful test transaction is a fix.
  6. Is the runbook stored in a place accessible when the primary network is down? If your runbook lives on a wiki that requires the VPN, and the VPN is what failed, you have a circular dependency.
  7. Was the runbook tested in the last quarter by someone who did not write it? Untested runbooks are documentation theater. Schedule a tabletop exercise. Break the system intentionally in a staging environment and hand the runbook to a colleague. Watch where they get stuck.
  8. Does the runbook include the name and contact method of the human who owns the service? Sometimes the script ends. The final scene of every runbook should be: “Escalate to [name] at [phone number]. State what you have checked and what the current system state is.”

The Generator Test

There is a final test I apply to every runbook I write. I call it the generator test. Imagine you are on call. The generator outside has been running for three hours. You have maybe ninety minutes of fuel left. The UPS is beeping. Your internet connection is a single 4G hotspot that drops packets whenever it rains. You are the only engineer awake within two time zones. Open your runbook. Can you follow it from symptom to resolution without once stopping to think, “What does this step mean?” If the answer is no, the runbook is not finished.

Most engineering advice assumes infrastructure that most of the world does not have. It assumes you can spin up a new instance, fail over to another region, or page a secondary on-call. Runbooks written under those assumptions are fragile. They break the moment the context shifts from “comfortable office with dual monitors” to “plastic chair in a server room with a dying generator.” Write your runbooks for the plastic chair. Write them as if the person reading them is you, at 2 AM, with everything on the line. That is the only audience that matters.

Debugging Intermittent Failures in Production: A Field Guide for Engineers Who Hate Guesswork

Intermittent failures in production are the worst. They don’t show up in staging. They vanish when you try to reproduce them. And yet, at 3 a.m., they yank you out of sleep with a pager alert that clears itself before you’ve even found your glasses. If you work on systems that actually matter—payment processing, telemetry pipelines, authentication services—you know the drill. The error rate spikes to 2% for six minutes, then flatlines. The logs spit out a timeout, a dropped connection, or a stack trace from a library you didn’t write. Your first instinct is to blame the network. Don’t. Let’s walk through a methodical approach that finds the real cause instead of just restarting the service and hoping for the best.

Server rack with blinking lights in a dark data center

Start With the Evidence You Already Have

Most teams skip this part. They jump straight to adding more logging or, worse, they start changing code. Before you touch anything, gather every piece of data from the affected time window. Pull application logs, infrastructure metrics, load balancer access logs, database slow query logs, and any distributed tracing data you have. Your job is to build a timeline of what the system was doing when things went sideways.

Look for correlation, not causation. Did the failure window line up with a deployment? A traffic spike? A cron job kicking off a batch process? A sudden drop in available database connections? I once spent two days chasing a bug that turned out to be a misconfigured backup script saturating a read replica’s disk I/O every Tuesday at 2:14 AM. The application logs screamed “connection timeout,” but the real culprit was a disk pegged at 100% for 30 seconds on a server nobody thought to monitor. The app was just the messenger.

Fix Your Observability Before You Fix the Bug

If you can’t answer basic questions about your system’s state during the failure window, your observability is broken. You need three things: metrics that show you what happened, logs that let you query specific events, and traces that connect requests across services. Most teams have metrics. Fewer have structured logs they can actually query. Almost nobody has tracing set up properly. Intermittent failures in distributed systems are nearly impossible to pin down without traces that show where a request spent its time and where it died.

For metrics, focus on the golden signals: latency, traffic, errors, and saturation. But don’t just stare at averages. Averages are liars. An intermittent failure affecting 1% of requests will be invisible in your P50 latency. You need percentiles—P95, P99, P999. If your P99 latency spikes while your P50 stays flat, you’ve got a tail latency problem. That’s your intermittent failure. Now you need to figure out what’s causing the tail.

Close-up of network cables plugged into a switch

Reproduce the Failure or Die Trying

Intermittent failures are intermittent because they depend on a specific set of conditions you haven’t identified yet. Your job is to find those conditions and recreate them. This is where most engineers give up and start guessing. Don’t be that engineer. Build a hypothesis and test it systematically.

Start with the simplest possible reproduction. If the failure involves a specific API endpoint, hammer that endpoint in a loop with varied payloads. If it involves a database query, run that query under load. If it involves a network call, simulate latency and packet loss. Tools like tc (traffic control) on Linux let you inject artificial delay, jitter, and packet loss onto network interfaces. A command like tc qdisc add dev eth0 root netem delay 100ms 20ms loss 1% can expose race conditions and timeout bugs that only surface when the network gets flaky.

If you can’t reproduce the failure in a test environment, you’re missing a variable. Check your production configuration. Are you using connection pooling? What are the timeout settings? Is there a circuit breaker that trips under certain conditions? I once burned three days trying to reproduce a connection reset error that only happened in production. The culprit was a 30-second idle timeout on a load balancer that didn’t match the 60-second keepalive on the application server. The load balancer was killing connections the app thought were still alive. A quick tcpdump on the production host caught the RST packet and solved the mystery in 20 minutes.

Use Production Traffic as Your Test Bed

Sometimes you can’t reproduce the failure outside of production because it depends on real traffic patterns, data shapes, or concurrency levels you can’t simulate. When that happens, you need to debug in production without breaking production. Feature flags, canary deployments, and traffic mirroring are your tools here. If you suspect a code change introduced the failure, roll it back with a feature flag and watch the error rate. If you suspect a performance regression, deploy a canary and compare its metrics to the stable version. If you need to test a fix, mirror a slice of production traffic to a test instance and see if the failure surfaces.

One technique I rely on heavily is adding targeted, high-signal logging to the failing code path. But don’t just log everything—that’s a fast track to drowning in noise and blowing up your log storage bill. Log the specific state that matters: the values of variables that control branching, the timing of critical sections, the exact error codes and messages from downstream dependencies. Use log sampling if the code path is hot. A 1% sample rate on a high-throughput endpoint will still give you hundreds of data points during a failure window.

Engineer analyzing server logs on multiple monitors

Common Causes and How to Isolate Them

After debugging hundreds of these failures, I’ve found they usually fall into a few buckets. Here’s how to spot each one.

Resource Exhaustion

File descriptors, memory, threads, database connections—every resource in your system has a hard limit. When you hit it, requests fail. The failures are intermittent because the exhaustion is often transient: a slow memory leak triggers garbage collection, a connection pool drains and refills, a thread pool backs up under load and then recovers. Check your metrics for any resource creeping toward its limit. Look at the shape of the curve, not just the current value. A file descriptor count climbing steadily over days is a leak. A thread pool maxing out during traffic spikes needs tuning or backpressure.

Timeout Mismatches

This is the single most common cause of intermittent failures in distributed systems. Service A calls Service B with a 5-second timeout. Service B calls Service C with a 10-second timeout. Service C is slow, so Service B waits 10 seconds, but Service A has already given up and closed the connection. Service B then tries to write the response to a closed socket and gets an error. The fix is to make timeouts consistent and shorter as you go deeper into the call chain. Every service should have a shorter timeout than the service calling it. This is called timeout propagation, and if you don’t have it, you will have intermittent failures.

Race Conditions

Race conditions are the hardest to debug because they depend on timing. Two requests hit the same code path at the same time, and the interleaving of their operations causes a failure. These often lurk in caching logic, database updates, or shared mutable state. To find them, look for code that reads a value, modifies it, and writes it back without proper locking or atomic operations. Check your ORM for optimistic locking bugs. Check your cache invalidation logic. If you’re using a language with concurrency primitives, review every goroutine, thread, or async task that shares state.

Downstream Degradation

Your service is fine. The database, message queue, or third-party API you depend on is not. Intermittent failures from downstream services often look like your own failures because the error surfaces in your code. The key is to check the dependency’s metrics and status page during the failure window. If you don’t have access to those, instrument every outbound call with the same golden signals you use for your own service. Record the latency, error rate, and saturation of every dependency. When the failure happens, you’ll see a spike in dependency errors or latency that correlates exactly with your own error spike.

Build a Postmortem That Actually Prevents Recurrence

Once you’ve found the root cause, document it. But don’t write a postmortem that just describes what happened and says “we’ll add more monitoring.” That’s useless. A good postmortem identifies the specific condition that caused the failure, explains why your existing defenses didn’t catch it, and lists concrete actions that will prevent that specific class of failure from happening again. If the failure was caused by a timeout mismatch, the action item isn’t “add monitoring for timeouts.” It’s “audit all service-to-service timeouts and enforce a consistent timeout propagation policy.” If the failure was caused by a file descriptor leak, the action item is “add a linter rule that flags missing close() calls and set up alerts for file descriptor usage above 80%.”

Also, update your runbooks. The next engineer who gets paged for this failure shouldn’t have to repeat your investigation. Write down the exact commands you ran, the metrics you checked, and the log queries you used. Include the specific values that indicate the failure is happening. A good runbook doesn’t say “check the database.” It says “Run SHOW PROCESSLIST; and look for queries in ‘Sending data’ state for more than 5 seconds.”

FAQ

Why do intermittent failures often happen at the same time every day?

This usually points to a scheduled job or a traffic pattern. Check your cron jobs, batch processes, and any automated tasks that run on a schedule. Also check your traffic patterns—many systems have daily peaks that can trigger resource exhaustion or race conditions. A database backup that runs at 2 AM and saturates the disk is a classic example.

How do I debug an intermittent failure that I can’t reproduce?

First, improve your production observability. Add structured logging to the failing code path with enough context to understand the state when the failure occurs. Use distributed tracing to see the entire request flow. If the failure is rare, consider increasing your log sampling rate temporarily or adding conditional logging that only fires when the error condition is met. You can also use a circuit breaker to capture the request payload and state when the failure occurs, then replay that request in a test environment.

What’s the difference between an intermittent failure and a flaky test?

An intermittent failure happens in production and affects real users. A flaky test is a test that sometimes passes and sometimes fails without any code changes. Flaky tests are often a sign of the same underlying issues—race conditions, timeout mismatches, or resource contention—but they surface in your test suite instead of production. Fixing flaky tests with the same rigorous root-cause analysis will prevent those failures from reaching production.

Should I add retries to handle intermittent failures?

Retries can mask the symptom but they don’t fix the root cause. Worse, naive retries can amplify the problem by adding more load to an already struggling system. If you add retries, make sure they’re exponential with jitter, have a maximum retry count, and are idempotent. But your first priority should always be to find and fix the underlying cause. Retries are a bandage, not a cure.