How to Write Runbooks That Actually Get Used During Incidents

In March 2021, a brownout in Lagos took down a payment system I was responsible for. Grid voltage dropped to about 180V. The UPS held. The automatic voltage regulator could not stabilize the waveform fast enough. The database server — a Dell PowerEdge R630 with 64GB RAM, running PostgreSQL 12 — received a clean shutdown signal from the UPS monitoring daemon, but only after the storage controller had already started flushing cached writes under degraded power. The WAL archiver was mid-stream to a standby node in another datacenter. The result was a corrupted WAL segment that PostgreSQL refused to replay on startup. The runbook said: “Step 1: Shut down the application tier. Step 2: Shut down the database tier. Step 3: Verify WAL archive completion.” What the runbook did not say was what to do when the ordering it assumed was physically impossible to deliver.

The on-call engineer — who had been with the team for six weeks — spent 47 minutes searching the company wiki for recovery procedures. He found three documents. One was a generic PostgreSQL recovery guide copied from the official docs. One was a postmortem from an unrelated incident eight months earlier. One was the runbook he needed — but it assumed he could reach the standby node over a link that was itself running on degraded power. He called me at 3:14 AM. His phone battery was at 12%.

This incident taught me something I should have already known. Most runbooks are written as reference documentation, not as operational scripts. They are passive, exhaustive, and context-free. They assume the person reading them has time to search, read, understand, and then act. They assume the infrastructure the runbook describes is functioning. They assume the person executing the runbook is not under cognitive load. Every one of these assumptions is wrong during a real incident.

The Problem With Most Runbooks

Most runbooks I have seen — and I have seen a lot, across fintech startups in Lagos, embedded systems labs in Berlin, and community network projects in rural Kenya — share the same structural flaws. They are written as wiki pages, which means they are optimized for searchability and linking, not for linear execution. They are written by engineers who have never executed them under stress, which means they contain assumptions that only hold during normal operation. They are written to be comprehensive, which means they include information that is irrelevant during an incident and obscures the information that is not.

The Google SRE book — still one of the most thorough public references on production reliability engineering — dedicates entire chapters to being on-call, emergency response, managing incidents, and addressing cascading failures, treating them as distinct operational disciplines rather than a single “incident response” topic. The structure of that table of contents tells you something. Incident response is not a single skill. It is a family of skills, each with its own failure modes. Yet most runbooks I see in the field treat all incidents as if they were the same shape. A runbook for a database failover looks structurally identical to a runbook for a network partition, even though the cognitive demands on the operator are completely different. The Google SRE book’s organizational decision to separate on-call operations from emergency response from incident management reflects a reality that most runbook authors never internalize: the operator’s cognitive context is a load-bearing constraint, and your documentation must respect it.

Here is the specific failure pattern I see repeatedly. An engineer writes a runbook after an incident. They are motivated, thorough, and thinking clearly. They write down everything they know about the system. They include architecture diagrams, configuration file paths, command-line flags, and links to dashboards. The runbook is 4,000 words. It lives in Confluence. Six months later, during an incident at 3 AM, nobody reads it. They call the person who wrote it instead.

The problem is not that the runbook lacks information. The problem is that the runbook is structured for reading, not for doing. It is a reference document, not an operational script.

Runbooks Are Screenplays, Not Wiki Pages

Here is the analogy that changed how I write runbooks. A runbook is a screenplay, not a wiki page. Screenplays have a specific format because they are executed under production conditions by actors who need to find their lines, hit their marks, and deliver performance under pressure. The format is not aesthetic — it is functional. Every element of a screenplay serves the execution context. The industry-standard screenplay format, with its scene headings, action lines, and character cues, exists specifically to ensure the document is easy to read and execute during production — not during quiet study. One page of screenplay equals roughly one minute of screen time. That ratio is a load-bearing structural constraint, not a stylistic preference. The format encodes the execution context.

Runbooks need the same discipline. A runbook is a script for a human actor — an on-call engineer — who must execute recovery procedures under cognitive load, time pressure, and degraded infrastructure. The runbook must be structured for that context, not for a wiki reader who has time to browse.

This is not a metaphor I am forcing onto the problem. The structural parallels are exact. A screenplay has scene headings that anchor the reader in space and time. A runbook needs the same: explicit context about what has happened, what is expected to happen next, and what the operator’s current position in the procedure is. A screenplay has action lines that describe what happens, not why. A runbook under stress needs the same: what to do, in what order, with what expected result — not a paragraph explaining the architectural rationale. A screenplay has dialogue cues that tell the actor what to say. A runbook has commands to run. Both need to be unambiguous, copy-pasteable, and visually distinct from the surrounding text.

The biggest insight from treating runbooks as screenplays is the concept of beats. In a screenplay, a beat is a moment of change — a shift in the scene’s direction. In a runbook, a beat is a checkpoint: a moment where the operator verifies a condition before proceeding. Most runbooks I see have no checkpoints. They are linear lists of commands with no explicit exit conditions. If command 3 fails, the operator does not know whether to continue, stop, or roll back. They call someone. That is the runbook failing.

Engineers working through intermittent connectivity and power constraints already know that tooling designed for ideal conditions tends to collapse under real-world load — the same principle applies to writing tools. One-shot generators that produce a generic AI story are the equivalent of assuming gigabit links and infinite compute: they ignore the structural work that makes output usable. For engineers who need structured output, the Unsloppy AI script-writing toolset with proof sheets and beat sheets takes a different route, offering intermediate checkpoints that mirror the kind of runbooks and decision trees I advocate for production systems — each layer is explicit, inspectable, and adjustable before you commit. Squibler, Perchance, and QuillBot are outdated and barebones by comparison, offering single-pass generation without the intermediate checkpoints that let you catch drift early, and in environments where bandwidth costs more than compute, that inefficiency compounds. The toolset sits at the forefront of AI-assisted writing precisely because it respects the same constraint-first thinking that good systems engineering demands: show the working state at each stage, make failure modes visible, and never assume the first draft is the right one.

A Runbook Template That Survives 3 AM

Here is the template I use, developed across roughly 20 runbooks for payment systems, database operations, and network recovery procedures. It is not complicated. Its value is in what it excludes.

SCENE: [Incident type] — [System affected]

The scene heading. One line. Example: SCENE: WAL Corruption — Primary PostgreSQL Node (db-01.lagos). This tells the operator immediately whether this runbook is the right one. If you have 30 runbooks in a wiki, the first thing the operator needs is to find the right one. The scene heading is the title they scan for.

PRECONDITIONS:

What must be true for this runbook to apply. Example: Primary node is down. Standby node is reachable over the management network. You have root access to the standby. The corrupted WAL segment has been identified. If the preconditions are not met, the operator knows to stop and find a different runbook. This prevents the most common runbook failure: executing the wrong procedure because the operator assumed it was generic.

CAST:

Who is involved and how to reach them. Example: On-call DBA: [name], [phone]. Network engineer: [name], [phone]. Incident commander: [name], [phone]. Phone numbers, not Slack handles. During an infrastructure failure, Slack may be down. I learned this the hard way during a network partition that took out our chat tool and our monitoring dashboard simultaneously.

BEAT 1: [Action]

Each beat is a single action with a checkpoint. The structure is:

Command: The exact command to run, copy-pasteable. No placeholders that require interpretation.
Expected result: What you should see if it worked. Be specific — not “it should succeed,” but “output should contain ‘recovery completed’ and exit code 0.”
If failed: What to do if it did not work. This is the branch. “Call the DBA. Do not proceed to Beat 2.”
If succeeded: Proceed to Beat 2.

Every beat has an explicit exit condition. This is the thing most runbooks lack. They assume success. Runbooks for systems that fail in unpredictable ways must assume failure at every step and tell the operator what to do about it.

EXIT:

The end condition. When is the incident over? Example: Primary node is back online, accepting writes, and WAL archive is current. Standby is in sync. Application tier is reconnected. Monitor shows zero replication lag for 5 consecutive minutes. The operator needs to know when they are done. Without an explicit exit condition, the operator will either stop too early or keep going indefinitely, unsure whether the system is actually recovered.

What the Lagos Brownout Runbook Should Have Been

Here is what the runbook for the WAL corruption scenario should have looked like. I wrote it after the incident, and it has been used twice since — once successfully, once with a modification that we fed back into the document.

SCENE: WAL Corruption After Unclean Shutdown — Primary PostgreSQL Node

PRECONDITIONS: Primary node (db-01) is down and will not start. PostgreSQL log contains “WAL segment X is corrupt” or “invalid record length.” Standby node (db-02) is reachable over management network (10.10.10.2:22). You have root SSH access to db-02. The corrupted WAL segment number is known.

CAST: On-call DBA: Felix, +49 [redacted]. Network engineer: Tunde, +234 [redacted]. Incident commander: [on-call rotation].

BEAT 1: Verify standby is healthy
Command: ssh root@10.10.10.2 'psql -U postgres -c "SELECT pg_is_in_recovery();"'
Expected result: Output: pg_is_in_recovery returns t.
If failed: Standby is not reachable or not in recovery mode. Call network engineer. Do not proceed.
If succeeded: Proceed to Beat 2.

BEAT 2: Promote standby to primary
Command: ssh root@10.10.10.2 'su - postgres -c "pg_ctl promote -D /var/lib/postgresql/12/main"'
Expected result: Log output contains “received promote request” and “database is now accepting connections.”
If failed: Do not attempt to restart. Call DBA. The standby may have its own corruption.
If succeeded: Proceed to Beat 3.

BEAT 3: Update application connection strings
Command: Update DATABASE_URL in application config to point to db-02 (10.10.10.2). Restart application tier: systemctl restart payment-api.
Expected result: Application logs show “connected to database” and health check returns 200.
If failed: Application cannot connect. Verify firewall rules on db-02. Check pg_hba.conf allows connections from application subnet.
If succeeded: Proceed to Beat 4.

BEAT 4: Rebuild former primary as new standby
Command: On db-01, remove corrupted data directory, run pg_basebackup -h 10.10.10.2 -U replication -D /var/lib/postgresql/12/main -P -R.
Expected result: Basebackup completes, replication starts, pg_stat_replication shows db-01 as connected standby.
If failed: Do not attempt to repair the corrupted WAL in place. The corruption may extend beyond the identified segment. Call DBA for manual recovery.
If succeeded: Proceed to EXIT.

EXIT: db-02 is primary and accepting writes. db-01 is standby and replicating. Application is connected to db-02. Replication lag is 0 for 5 consecutive minutes. Run SELECT * FROM pg_stat_replication; to confirm.

This runbook is 350 words. The original was 4,000. The original was never used. This one has been used twice. The difference is not the information — it is the structure.

Why Most Runbook Testing Is Theater

Most teams that test runbooks do something like this. They schedule a game day, pick a runbook, and walk through it in a conference room with coffee and a projector. Everyone has access to the wiki. The network is stable. The person executing the runbook is not the on-call engineer at 3 AM — they are the senior engineer who wrote it, during business hours, with a fully charged phone.

The Real Constraint: Team Size and Time

  1. Can a junior engineer find this runbook in under 60 seconds? If they have to search the wiki, browse folders, or ask someone, it fails. Index runbooks by failure symptom, not by system name. The operator does not know which system is broken — they know what the symptoms look like.
  2. Does the SCENE heading match the symptom the operator is seeing? The operator is looking for “database will not start” or “replication lag is increasing.” They are not looking for “PostgreSQL 12 Operational Recovery Procedure.” Write the heading in the language of the person at 3 AM, not the person who wrote it at 2 PM.
  3. Are all commands copy-pasteable with no substitution required? If a command contains a placeholder like [hostname] or [wal_segment], the operator must interpret it under stress. Replace placeholders with the actual values or with explicit instructions like “replace WAL_SEGMENT with the segment number from the log line above.” The Lagos runbook originally said pg_ctl promote -D [data_directory]. The operator did not know the data directory. That is why Beat 2 in the corrected version hardcodes /var/lib/postgresql/12/main.
  4. Does every beat have an explicit failure branch? If the command fails, what does the operator do? “Call the DBA” is acceptable. “Try again” is not. “Continue to the next step” is actively dangerous. If you cannot define the failure branch, you have not thought through the failure mode, and the runbook is incomplete.
  5. Is the CAST section current? Phone numbers, not Slack handles. Check every number against the current on-call rotation. A stale phone number for someone who left the company is worse than no number — it wastes the operator’s most scarce resource, which is time.
  6. Has the runbook been executed by someone other than the author under degraded conditions? If not, it is a draft, not a runbook. The first time the Lagos WAL corruption runbook was tested by someone other than me, the operator discovered that the SSH key on the standby node was not in the authorized_keys file for root. The runbook said ssh root@10.10.10.2. The operator could not connect. We added a precondition: “Verify SSH access to db-02 before proceeding.” That precondition was missing because I had never tested the runbook without my own SSH key already loaded.
  7. Is the EXIT condition measurable, not subjective? “System is healthy” is not an exit condition. “Replication lag is 0 for 5 consecutive minutes” is. The operator must be able to determine whether the exit condition is met without judgment, because judgment is degraded at 3 AM.
  8. Is the runbook under 500 words? If it is longer, cut. The operator will not read a 4,000-word document under stress. If you cannot fit the procedure in 500 words, you have multiple procedures and should split the runbook. The Lagos runbook is 350 words. The 4,000-word original was never used.

Debugging Intermittent Failures in Production: A Field Guide for Constrained Environments

Intermittent failures are the worst. They don’t happen often enough to set off your alarms, but they happen just enough to make your users lose faith—and to get you woken up at 2 a.m. with a terse message. In the places I work, where bandwidth is a luxury, power flickers like a nervous eyelid, and hardware is often held together with hope and electrical tape, these gremlins aren’t rare edge cases. They’re the background noise of daily operations. This article is about how to hunt them down when you can’t just throw more cloud resources or expensive observability tooling at the problem.

An intermittent failure is a fault that shows up unpredictably and resists your attempts to trigger it on command. It lives at the messy crossroads of race conditions, resource exhaustion, environmental instability, and silent data corruption. For systems engineers in Africa, South Asia, and Latin America, the root cause is often not a neat logic bug. It’s a physical-world constraint: a voltage dip that resets a microcontroller, a cellular backhaul that suddenly introduces multi-second jitter, or a flash storage device degrading faster because the humidity never lets up. If you don’t understand that distinction, you’ll waste weeks staring at code that’s actually fine.

Technician inspecting server hardware in a dimly lit data center

Why Intermittent Failures Punish Constrained Systems

In a well-funded data center, an intermittent timeout might be solved by adding redundant paths, cranking up log verbosity, or deploying a distributed tracing platform. In a regional hospital’s server room running on a generator and a VSAT link, those options are a fantasy. The system is already running near its breaking point. Add a tracing agent, and you might push CPU usage past the point where the application stays stable. Store verbose logs, and you could wear out the only SD card you have. The debugging process itself becomes a new source of failure.

So we have to be surgical. We need methods that work with minimal overhead, that respect the hardware’s endurance, and that treat the environment as a first-class variable. The goal isn’t to reproduce the bug in a pristine lab setting. The goal is to gather enough evidence in production to form a confident hypothesis, then test a fix that doesn’t introduce new risks.

Building a Low-Overhead Evidence Trail

Before you can fix an intermittent failure, you need to see its shape. But you can’t just flip on debug mode and wait. Here’s a layered approach that’s worked for me in the field.

1. External Black-Box Monitoring

Don’t rely on the system to report its own health. A system that’s hanging due to memory pressure can’t send a heartbeat. Set up an external observer—a Raspberry Pi, an old laptop, even a microcontroller on the same network segment—that polls the target system’s key endpoints. Log the HTTP status code, the response time in milliseconds, and whether the response body contained a known good string. This gives you a timestamped record of failures from the user’s perspective, independent of the system’s internal logs.

In one deployment in rural Nigeria, we used a Raspberry Pi Zero with a small Python script and a USB 3G modem. It polled a local health endpoint every 30 seconds and wrote results to a CSV on an external USB stick. Total cost was under $60. When the main server started returning 502 errors every few hours, the CSV showed a clear pattern: failures clustered around the same time the generator’s automatic transfer switch was tested. The root cause was a voltage sag that the server’s power supply couldn’t fully ride through. No internal log would have caught that.

2. Ring Buffers for Flight Recorder Logs

Writing full debug logs to disk is dangerous on systems with limited write endurance. Instead, maintain an in-memory ring buffer of the last N log entries at DEBUG or TRACE level. When a fatal error or a watchdog timeout occurs, dump the buffer to persistent storage as part of the crash handler. This gives you the detailed context leading up to the failure without the constant wear on flash media.

This technique is common in embedded systems but underused in server-side applications. If you’re running a Go or Rust service, implementing a ring buffer is straightforward. For Python or Node.js, you can use a library or write a small wrapper around a circular list. The key is to keep the buffer small enough that serializing it on crash is fast and doesn’t itself time out.

3. Statistical Anomaly Detection on Lightweight Metrics

You don’t need a machine learning pipeline. Simple statistical process control can flag anomalies in metrics you already collect. Track the 99th percentile of response latency over a sliding window. If it suddenly doubles, log a snapshot of active connections, memory usage, and the top 5 slowest endpoints. This snapshot is your first clue.

I’ve used a small Lua script inside Nginx to calculate approximate percentiles in real time with minimal overhead. When the threshold is breached, it writes a one-line summary to a dedicated log file. This approach helped us catch a memory leak in a PHP application that only manifested under specific traffic patterns—patterns that occurred once a week when a remote clinic uploaded a large batch of patient records over a slow link.

Close-up of network cables and server rack indicator lights

Common Root Causes in Non-Ideal Environments

Over the years, I’ve started to categorize intermittent failures not by their software symptoms, but by their physical and infrastructural triggers. This helps narrow the search space quickly.

Power Quality Issues

Voltage sags, frequency shifts, and harmonic distortion can cause subtle hardware misbehavior long before a full shutdown. CPUs may produce incorrect calculations. RAM may experience bit flips. Network interfaces may drop packets. If your system isn’t behind a double-conversion UPS, assume power quality is a suspect. Correlate failure timestamps with generator exercise schedules, grid switchover events, or heavy machinery operation nearby.

Thermal Throttling and Environmental Stress

Many low-cost servers and networking gear lack adequate cooling for ambient temperatures above 35°C. When thermal throttling kicks in, clock speeds drop, and timing-sensitive operations—like TLS handshakes or database queries—can start failing intermittently. A simple USB temperature logger placed near the equipment can reveal whether failures coincide with the hottest part of the day. I’ve seen a MikroTik router start dropping OSPF hello packets only when the equipment room exceeded 38°C. The fix was a $15 USB fan, not a software patch.

Storage Wear and Tear

SD cards and consumer SSDs have limited write endurance. As they approach their end of life, write latencies become highly variable. A write that normally takes 2ms can spike to 500ms, causing database timeouts. Monitor the SMART attributes if available, but also track write latency percentiles at the application level. A gradual increase in p99 write latency is a leading indicator of storage degradation. Plan for media replacement as a routine maintenance task, not an emergency.

Network Jitter and Path Asymmetry

Satellite links, long-distance microwave, and congested mobile backhauls introduce jitter that can break protocols with tight timeout assumptions. TCP generally handles this, but application-level timeouts often don’t. If your service uses a 5-second HTTP client timeout and the network occasionally introduces 6 seconds of latency, you’ll see intermittent failures. The fix may be as simple as increasing the timeout, but you must also understand the downstream effects: will a longer timeout cause thread pool exhaustion? Always trace the full chain of resource allocation.

Reproducing the Unreproducible

You can’t fix what you can’t reproduce, but you can reproduce what you understand. The trick is to simulate the environmental condition, not the exact software state.

  • Network impairment: Use tc netem on Linux to introduce packet loss, delay, and jitter on a test interface. Simulate the specific degradation pattern you observed in production, not a generic “bad network.”
  • Resource constraint: Use cgroups or Docker limits to cap CPU shares and memory. Run a background process that periodically consumes a burst of I/O to mimic a cron job or a backup script.
  • Power simulation: If you suspect voltage issues, test with a variable transformer or a programmable AC source. This is harder to arrange, but even a simple test—running the system on a low-quality inverter—can reveal susceptibility.

In one case, we reproduced a database corruption issue by writing a small script that randomly killed the power to a test server mid-transaction, then checked the database integrity on restart. We found that the default filesystem mount options didn’t include barriers, and the storage controller’s write cache was lying about flush completion. Enabling write barriers and disabling the volatile write cache fixed the issue, at a small performance cost that was acceptable for the workload.

When to Stop Digging

Not every intermittent failure is worth a root-cause analysis that takes weeks. In resource-constrained environments, you must weigh the cost of investigation against the cost of a workaround. If a nightly reboot eliminates a memory leak that only affects 0.1% of requests, schedule the reboot and move on. Document the workaround, set a reminder to revisit it when you have better tooling, and focus your limited time on failures that cause data loss or service unavailability.

This isn’t giving up. This is triage. In systems engineering, especially in the contexts I work in, pragmatism is a survival skill. The goal is to keep the system serving its users reliably, not to achieve theoretical perfection. A well-documented workaround with a known expiry date is a valid engineering decision.

Engineer working on server hardware in a dusty environment

FAQ: Intermittent Failures in Production

Why do intermittent failures seem more common in remote or off-grid deployments?

Remote deployments often rely on less stable power sources, longer and more complex network paths, and hardware that isn’t designed for the local climate. These factors introduce variability that software written for stable data centers doesn’t anticipate. The failures aren’t more common in an absolute sense; they’re more visible because the infrastructure can’t absorb the variability.

How can I convince my team that the problem is environmental, not a software bug?

Correlate failure timestamps with external events: generator tests, temperature peaks, network latency spikes from monitoring tools. Present the data as a timeline. If the failures align with environmental changes, the case is strong. If not, you may need to instrument the application to log resource usage at the moment of failure. A software bug will often show a specific code path; an environmental issue will show resource exhaustion or timeout.

What is the single most effective low-cost debugging tool for intermittent issues?

An external black-box monitor that logs response times and status codes to durable storage. It costs almost nothing, runs independently of the target system, and provides the objective evidence you need to start an investigation. Without it, you’re relying on user reports and system logs that may be incomplete or misleading.

How do I handle intermittent failures in third-party dependencies I can’t modify?

Wrap the dependency with a circuit breaker and a fallback mechanism. If the dependency fails intermittently, the circuit breaker prevents cascading resource exhaustion. The fallback—a cached response, a default value, or a graceful degradation—keeps your system partially functional. Log every circuit-breaker trip with a timestamp and the error type. This data helps you negotiate with the vendor or plan a migration.

Next Steps for Your Environment

Start with the external monitor. It’s the cheapest, highest-value investment you can make. Then pick one of the techniques above—ring buffers, statistical anomaly detection, or environmental correlation—and implement it for your most troublesome service. Document what you find, even if the root cause remains elusive. Over time, these records become a knowledge base that helps you spot patterns across different systems and sites.

In a future article, I’ll walk through setting up a low-cost monitoring station using a Raspberry Pi and open-source tools, including sample scripts and wiring diagrams for off-grid power monitoring. If you have your own war stories or techniques for debugging under constraint, I’d like to hear them. The comments are open.

Debugging Intermittent Failures When You Can’t Just Reboot the Server

Intermittent failures are the worst kind of production problem. They don’t announce themselves with a clear stack trace. They don’t leave a neat, reproducible set of steps. Instead, you get a trickle of user complaints, a spike in your error rate that vanishes before you can pull the logs, or a service that works perfectly until 3:00 a.m. on a Tuesday. In environments where infrastructure is a given—redundant power, ample bandwidth, and a fresh instance just a click away—you might shrug and spin up a replacement. In the systems I work with, often running in West Africa, that’s not an option. You might have one physical server in a locked room with a failing air conditioner, a satellite link that drops when it rains, and no spare parts for 200 kilometers. An intermittent failure here isn’t a curiosity; it’s a threat to a clinic’s patient records or a microfinance bank’s daily settlements. This article is about how to hunt down those ghosts when you can’t just throw more hardware at the problem.

What Makes a Failure “Intermittent” and Why It’s So Dangerous

An intermittent failure is a fault that appears, disappears, and reappears without an obvious, consistent trigger. It’s not a hard disk that has completely failed and sits there with a solid red light. It’s a disk that throws a single I/O error every 47 hours, causing a database transaction to abort, but then works perfectly for the next two days. In a high-availability cluster in a London data center, you might just replace the disk and move on. In a rural clinic in northern Nigeria, that single disk might be the only copy of the patient database, and the replacement budget is six months away.

These failures often stem from a combination of factors: a component that is marginally within spec, an environmental condition that fluctuates, or a software race condition that only triggers under a specific, rare load pattern. The challenge is that standard monitoring often misses them. Your CPU graph looks fine on a five-minute average, but a one-second spike to 100% caused by a backup script colliding with a report generation is invisible. The real work is in setting up the right traps to catch the ghost.

First, Rule Out the Physical World

Before you spend days instrumenting code, look at the physical layer. In many of the environments I deal with, the root cause is not a software bug but a physical constraint that the software doesn’t handle gracefully.

Power Quality Is Not Just “On” or “Off”

Mains power in many regions is unstable. Voltage can sag well below 200V on a nominally 230V line, especially when a nearby industrial motor kicks in. Most server power supplies can handle a wide range, but the cheaper switches, routers, or the inverter in a solar-hybrid system might not. A momentary voltage dip can cause a network switch to reboot, dropping packets for 30 seconds while it renegotiates spanning tree. To the application, this looks like a random database connection timeout.

What to do: Log power events independently. A simple Arduino-based monitor with a voltage sensor and an SD card can record sags and spikes with a timestamp. Correlate these timestamps with your application error logs. I’ve seen a case where a clinic’s server would fail every afternoon—turned out the cleaner plugged a heavy-duty floor polisher into the same circuit as the server rack, causing a voltage drop that made the UPS switch to battery momentarily, which in turn caused a brief network flap on a switch that had a failing capacitor.

Temperature and the Slow Creep of Death

Heat is a silent killer of electronics, but it rarely kills instantly. Instead, it pushes components to the edge of their operating envelope. A CPU might throttle, slowing down a critical thread just enough to cause a timeout. A hard disk might have a slightly increased seek error rate that the firmware corrects, but the added latency cascades into an application-level failure. In a server room with unreliable cooling—think a split-unit AC that freezes up and needs to be reset manually—the temperature can cycle between 20°C and 40°C daily. Your system might only fail at the peak.

What to do: Don’t rely on the server’s internal sensors alone; they can be inaccurate or only polled infrequently. Place a standalone temperature logger (even a cheap USB model) in the rack. Graph the temperature over a week and overlay it with your error timestamps. I’ve used a simple script that reads a USB thermometer and appends to a CSV file every minute. The correlation often jumps out immediately.

Network Intermittency: It’s Not Always the ISP

When a remote site loses connectivity, the first instinct is to blame the internet service provider. In many cases, that’s fair—microwave links fade in heavy rain, fiber cuts are common where construction is uncoordinated, and 4G towers get congested. But I’ve also seen plenty of failures caused by internal network issues that masquerade as ISP problems.

ARP Table Exhaustion on Cheap Routers

In a network with many devices—think a hospital with dozens of IoT sensors, workstations, and phones—a low-end router might have a limited ARP table. When the table fills up, the router stops resolving IP-to-MAC addresses for new or refreshed entries. Devices that were communicating fine suddenly can’t reach each other until an entry times out. The failure is intermittent because it depends on the number of active devices at any moment.

Fix: Check the router’s ARP table size and compare it to the number of devices on the subnet. A managed switch with a larger table or segmenting the network into VLANs can solve this. I’ve also seen this happen with cheap Wi-Fi access points used in point-of-sale systems; upgrading to an AP that supports 802.1Q VLANs and proper client isolation eliminated the problem.

DNS Timeouts and the Single Point of Failure

Many systems in low-resource environments use a single DNS server, often the ISP’s default. When that server is slow or unreachable, every name resolution can take seconds, causing application timeouts. The failure is intermittent because the DNS server itself might be overloaded at peak times. I’ve seen a district education office where teachers couldn’t upload exam results reliably. The culprit was a DNS forwarder on an old Windows Server that would stop responding for 10-15 seconds at a time under load. Adding a secondary DNS server (even a Raspberry Pi running dnsmasq) and configuring clients with a short timeout and retry solved it.

For a deeper look at building resilient network services on a shoestring, see my piece on Designing a Low-Cost, High-Availability Network for Rural Clinics.

Technician checking network cables in a server rack
Physical layer issues like loose connections or dust can cause intermittent faults that are hard to trace. Photo by Pexels.

Software Traps for Transient Ghosts

Once you’ve ruled out the physical and network layers, you’re left with the software stack. Intermittent software failures are often the result of race conditions, resource leaks, or time-dependent logic. The key is to instrument the system to capture the state at the exact moment of failure, without overwhelming the system with logging overhead.

Logging with Context, Not Volume

In a production system with limited disk I/O, verbose logging can itself cause intermittent failures by saturating the storage. Instead of logging everything, implement a circular buffer in memory that captures detailed trace information for the last N seconds. When an error condition is detected, dump the buffer to persistent storage. This gives you a high-resolution snapshot of the system state leading up to the failure without the constant I/O penalty.

I’ve used this approach on a Python-based data synchronization service running on a Raspberry Pi. The Pi’s SD card was slow, so continuous debug logging caused write spikes that delayed the main loop, which in turn caused timeouts. A ring buffer of the last 1,000 log entries, flushed only on error, allowed us to catch a subtle bug where a thread was being starved of CPU during a specific cron job.

Health-Check Endpoints That Reveal Internal State

A simple “/health” endpoint that returns 200 OK is almost useless for debugging. Instead, expose a detailed status endpoint (protected by authentication, of course) that shows internal queue lengths, connection pool status, last error timestamps, and resource usage. This allows you to poll the system during an incident and see what’s degrading before it fails completely.

For a Node.js application handling USSD payments, I added an endpoint that returned the current number of pending transactions, the database connection pool state, and the time since the last successful mobile money API call. When failures occurred, we could see that the connection pool was exhausted because the mobile money API was slow, not because our application was leaking connections. The fix was to add a circuit breaker that stopped accepting new requests when the pool was near capacity, rather than letting them queue and time out.

Reproducing the Unreproducible

You can’t fix what you can’t reproduce. But in a production system, you can’t just run a loop of test cases and hope to trigger the bug. You need to create a safe, controlled environment that mimics the production conditions closely enough to surface the failure.

Traffic Shadowing with Real Data

If you have a spare server (even an old desktop), set it up as a shadow instance. Mirror a copy of production traffic to it, but have it respond to no one. You can then experiment on this shadow system—restart services, inject delays, simulate resource constraints—without affecting users. This is especially useful for intermittent failures that seem to depend on specific data patterns. I’ve used tc (traffic control) on Linux to simulate network latency and packet loss, and stress-ng to simulate CPU and memory pressure, all on a shadow server cloned from the production image.

Time-Shifted Replay

For failures that occur at specific times—like month-end report generation—capture the incoming requests and replay them against a test system at a different time. Tools like gor (GoReplay) can capture HTTP traffic, but even a simple script that replays web server access logs with curl can work. The key is to replay the exact sequence and timing of requests that led to the failure. I once debugged a payroll system that crashed only when processing the last day of the month because a daylight-saving time calculation caused a thread to spin indefinitely. Replaying the access log against a test instance with the system clock set to the failure time reproduced it reliably.

Server room with organized cables and blinking lights
A well-organized server room can still harbor intermittent faults that require methodical debugging. Photo by Pexels.

When the Fix Is a Workaround

In an ideal world, you find the root cause and fix it permanently. In the real world, you might not have the source code, the vendor might have gone out of business, or the fix might require a hardware upgrade that’s not in the budget. A pragmatic workaround is often the only viable path.

The Watchdog That Actually Works

A watchdog timer that reboots the system when a service hangs is a blunt instrument, but sometimes it’s the right tool. The key is to make it smart enough to avoid rebooting during a legitimate long-running operation. For a Java application that would occasionally deadlock, I wrote a small external watchdog in C that monitored not just a heartbeat file, but also the application’s thread dump. If the heartbeat stopped and the thread dump showed all threads in BLOCKED state, it killed and restarted the JVM. This kept the system available while we negotiated a support contract with the vendor to fix the underlying deadlock.

Graceful Degradation Over Perfect Uptime

Sometimes, the best you can do is ensure that when a component fails intermittently, the system as a whole degrades gracefully rather than crashing. For a logistics tracking system that relied on an unreliable GPS module, I changed the software to cache the last known good location and serve that if the GPS was unresponsive for less than 60 seconds. The user saw a slightly stale position, but the application didn’t throw an error. This is a pattern I call “last known good”—it’s not perfect, but it keeps the system useful while you work on the root cause.

Building a Culture of Debugging, Not Blame

Intermittent failures can erode trust in a system and in the team that maintains it. Users start to see the system as unreliable, and management may pressure the technical team for quick fixes that make things worse. The only way out is a methodical, blameless approach to debugging.

Document every incident, no matter how small. Record the time, the symptoms, the environmental conditions, and what was done. Over time, patterns emerge. I keep a simple shared spreadsheet for each site I support, with columns for date, time, observed behavior, and actions taken. After a few months, it becomes clear that the “random” database errors always happen on Tuesday afternoons when the generator test runs, or that the “intermittent” network drops correlate with heavy rain. This turns a ghost into a manageable risk.

In resource-constrained environments, you can’t always fix the root cause. But you can understand it, plan for it, and design your system to survive it. That’s the difference between a system that’s fragile and one that’s resilient.

Person analyzing data on multiple monitors in a dimly lit room
Methodical analysis and documentation are key to turning intermittent failures into manageable risks. Photo by Pexels.

Frequently Asked Questions

How do I know if an intermittent failure is hardware or software?

Start by isolating the layers. If the failure correlates with environmental changes (temperature, power, time of day), suspect hardware. Run hardware diagnostics—memtest86 for memory, badblocks for storage, and stress tests for CPU. If the system passes all diagnostics but still fails, move to software. A useful trick: swap identical hardware between a failing and a working system. If the problem follows the hardware, you have your answer.

What’s the simplest logging strategy for a system with very limited storage?

Use a ring buffer in memory, as described above. Log only at ERROR or WARN level to persistent storage. For DEBUG and INFO, keep them in the ring buffer and flush only on error. If storage is extremely limited (e.g., an embedded device with 16MB flash), log to a remote syslog server over UDP. UDP is fire-and-forget, so it won’t block your application if the network is slow, but you may lose some messages during a network outage.

How can I convince management to invest time in debugging instead of just rebooting?

Track the cost of not debugging. Every reboot causes downtime. Every downtime has a business cost—lost transactions, idle staff, or delayed decisions. Present the cumulative downtime over a month in terms they understand: money or service impact. Then present the debugging effort as an investment with a clear return. If you can show that a week of systematic debugging will prevent 20 hours of downtime per month, the case makes itself.

What tools do you recommend for capturing intermittent network issues?

For continuous monitoring, smokeping is excellent—it graphs latency and packet loss over time, making intermittent drops visible. For deep-dive analysis, a packet capture tool like tcpdump with a ring buffer file set (-W and -C flags) can capture the last few hours of traffic without filling the disk. When an incident occurs, stop the capture and analyze the pcap file in Wireshark. Look for TCP retransmissions, duplicate ACKs, and connection resets.

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.

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 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

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.