Intermittent failures in production are the worst kind of bug. They donât show up in staging. They laugh at your unit tests. They appear at 3 a.m., trigger a PagerDuty alert, and vanish before you can even open a log stream. If youâre reading this, youâve probably been burned by one. I have, more times than I can count. This article is a direct, technical walkthrough of how I approach these problemsâno fluff, no theory that falls apart under real traffic. Weâll talk about patterns, tooling, and the specific steps that actually lead to a root cause.
Why Intermittent Failures Are Different
A deterministic bug is a gift. You reproduce it, you fix it, you move on. An intermittent failure is a statistical event. It might happen once per 10,000 requests. It might only occur when a specific database replica is under memory pressure and a particular userâs session token expires mid-request. The failure is a symptom of a system state that aligns just wrong. Your job is to reverse-engineer that state from sparse evidence.
Most engineers waste time on two dead ends: restarting services until the problem goes away, or adding more logging and hoping to catch it next time. Restarting destroys the state you needed to inspect. Blind logging adds noise and can mask the issue by changing timing. You need a structured hunt.
Step 1: Define the Failure Signature Precisely
Before touching any code or infrastructure, write down exactly what you know. Not âthe API returns 500s sometimes.â Thatâs useless. You need: the exact HTTP status code, the error message body, the upstream service that generated it, the time window, the affected endpoint, and any correlation with deployment events, traffic spikes, or cron jobs. If you have an error tracking system like Sentry or Rollbar, group by fingerprint and look at the distribution over time. A flat line with sudden spikes points to an external trigger. A slow, creeping increase suggests a resource leak.
If you donât have a fingerprint, create one. Hash the stack traceâs top three frames plus the exception type. This collapses what looks like a thousand different errors into a handful of actual root causes. Iâve seen teams chase 50 different stack traces that all boiled down to a single connection pool exhaustion bug.
Step 2: Instrument the Failure Path, Not the Whole System
Resist the urge to add logging everywhere. Target the exact code path that produces the error. If your API returns a 502 from an upstream service, instrument the outbound HTTP call with: request duration, target host, response status, and a correlation ID that ties back to the original request. If the failure is a database timeout, log the query text, the query plan hash, the connection pool stats at the moment of the timeout, and the row count estimate from EXPLAIN.
Use histograms, not averages. A p99 latency of 2 seconds with a p50 of 50ms tells you something very specific: most requests are fine, but a small fraction hit a slow path. That slow path is your intermittent failure. Prometheus histograms with meaningful buckets (not the defaults) will show you the shape of the problem. Set buckets at 10ms, 50ms, 100ms, 500ms, 1s, 5s, and 10s for an endpoint that normally responds in 100ms. The tail will jump out.

Step 3: Trace the Entire Request, Not Just Your Service
Intermittent failures often cross service boundaries. Your service might be fine, but the authentication service it calls is experiencing garbage collection pauses. Without distributed tracing, youâll never see that. If youâre not running something like Zipkin or Jaeger, start. Even a minimal implementationâpropagating a trace ID through HTTP headers and logging it at each hopâgives you the ability to filter logs by a single failed request and see every service it touched.
When you have a trace of a failed request, look for the hop where latency suddenly balloons. Thatâs your bottleneck. Then look at what was happening on that specific host at that specific second. Was CPU throttled? Was there a network retransmit spike? This is where host-level metrics become essential.
Step 4: Correlate with Infrastructure Metrics at the Same Granularity
Application logs tell you what happened. Infrastructure metrics tell you why. You need CPU utilization, memory usage, disk I/O wait, and network errors for the exact hosts that served the failed requestâat the same one-second resolution if possible. CloudWatch, Stackdriver, or your Prometheus node exporter can provide this. Donât look at cluster averages. A single bad node can cause 1% of requests to fail while the cluster dashboard looks green.
Pay special attention to CPU throttling in containerized environments. If your container has a 0.5 vCPU limit and the hostâs CPU is oversubscribed, your process can get throttled for milliseconds at a time. Those throttling events show up in cgroup metrics (cpu.stat) and correlate perfectly with timeout errors. I once spent two weeks chasing a ârandomâ Redis timeout that turned out to be CPU throttling on the Redis pod because the Kubernetes limit was set too low.
Step 5: Reproduce by Recreating the State, Not the Request
Most engineers try to reproduce an intermittent failure by replaying the same request over and over. That rarely works because the request isnât the triggerâthe system state is. Instead, recreate the state you suspect. If you think connection pool exhaustion is the cause, write a script that opens connections and doesnât close them until the pool is full, then send a real request. If you suspect a race condition under high concurrency, use a load generator like Vegeta or wrk2 to hit the endpoint with a precise request rate while varying the number of concurrent connections.
For database-related failures, restore a production snapshot to a staging database and replay a sample of real production queries at production speed. Tools like pg_replay for PostgreSQL or Percona Playback for MySQL can do this. The goal is to make your staging environment as close to the failing production state as possible, not just the same code.

Step 6: Use Production Traffic Mirroring as a Last Resort
When you canât reproduce the issue in staging, mirror a fraction of live production traffic to a canary instance that has extra diagnostics enabled. This is riskyâyouâre adding load to a system thatâs already strugglingâbut itâs often the only way to catch a bug that depends on real user behavior. Use a tool like GoReplay or Envoyâs traffic mirroring to send a copy of requests to a debug instance. On that instance, enable verbose logging, core dumps, or a debugger. The mirror instance doesnât respond to clients, so a crash or slowdown wonât affect users.
Iâve used this to catch a race condition in a session store that only manifested under a specific sequence of concurrent reads and writes that no load test ever generated. The debug instance logged the interleaving, and the bug was obvious in hindsight.
Step 7: Fix the Root Cause, Not the Symptom
Once youâve identified the trigger, fix it permanently. If itâs a connection pool exhaustion, donât just increase the pool sizeâfind out why connections are leaking or held too long. If itâs a race condition, donât slap a mutex on it and call it a dayâunderstand the data flow and fix the ordering guarantee. If itâs a resource limit, adjust the limit but also add monitoring to catch the condition before it becomes a user-facing error.
Document the failure mode, the detection method, and the fix. The next engineer who hits a similar intermittent failure will need that breadcrumb trail. Link the postmortem to your runbooks. If you fixed a database timeout caused by a missing index, add a check for missing indexes to your deployment pipeline. Make the fix systemic.
Common Patterns and Their Signatures
Connection Pool Exhaustion
Symptoms: Sporadic timeouts or âconnection refusedâ errors, often clustered in time. Error rate spikes and then recovers without intervention. Affects a specific service or database client.
Detection: Monitor pool utilization (active connections / max connections). Alert when utilization exceeds 80% for more than 60 seconds. Log stack traces of connection acquisition timeoutsâtheyâll show which code path is holding connections.
Fix: Identify leaking connections (code paths that donât return connections to the pool in finally blocks). Increase pool size only as a stopgap. Add circuit breakers to upstream callers so they fail fast instead of piling up.
Garbage Collection Pauses
Symptoms: Request latency spikes that exceed your timeout thresholds, causing 504 or 502 errors. No increase in error rate from the application itselfâthe errors come from load balancers or upstream services that timed out waiting.
Detection: Enable GC logging with timestamps. Correlate GC pause times with request latency spikes. In Go, use GODEBUG=gctrace=1. In Java, use -Xlog:gc*:file=gc.log. In .NET, use DOTNET_GCConserveMemory or GC logging. Look for âstop-the-worldâ pauses exceeding your request timeout.
Fix: Tune GC to reduce pause times (e.g., G1GC with MaxGCPauseMillis in Java, GOGC tuning in Go). Reduce allocation rate in hot paths. Consider value types or object pooling.
Race Conditions Under Load
Symptoms: Null pointer exceptions, index out of bounds, or corrupted data that occur only under high concurrency. Errors are non-deterministic and hard to reproduce with single requests.
Detection: Run your service under ThreadSanitizer (C/C++/Go) or similar race detectors in a staging environment with production-like concurrency. Log the sequence of operations leading to the crash. Use a deterministic simulation framework if available.
Fix: Identify the shared mutable state. Add proper synchronization or redesign to avoid shared state entirely (immutable data structures, actor model, single-writer principle).
Resource Throttling in Containerized Environments
Symptoms: Latency spikes and timeouts that correlate with CPU throttling or memory pressure on specific pods. No application-level errors loggedârequests simply time out.
Detection: Monitor cgroup CPU throttling metrics (container_cpu_cfs_throttled_seconds_total in Prometheus). Check for OOMKilled events in pod history. Look at node-level CPU steal time if using shared tenancy.
Fix: Increase CPU limits or remove them entirely if the workload is bursty. Set appropriate memory limits with headroom. Use guaranteed QoS class in Kubernetes for critical services.

Tooling Stack I Actually Use
Hereâs what I reach for when an intermittent failure lands in my lap. This isnât a sponsored listâthese are tools that have proven themselves in production trenches.
- Metrics: Prometheus with Grafana dashboards. Histogram buckets tuned per endpoint. RED metrics (Rate, Errors, Duration) for every service.
- Logging: Structured JSON logs shipped to Elasticsearch. Every log line has a trace ID, span ID, and service name. Kibana for ad-hoc queries.
- Tracing: Jaeger with adaptive samplingâsample 100% of errors and 1% of successes. This catches the failures without drowning in data.
- Profiling: Parca or Pyroscope for continuous profiling. When latency spikes, I can pull a flame graph from the exact time window and see where CPU time went.
- Traffic Mirroring: GoReplay for HTTP services, with a filter to mirror only requests matching the failing endpoint pattern.
- Load Generation: Vegeta for constant-rate load, wrk2 for coordinated omission-free latency histograms.
Building a Culture That Handles Intermittent Failures
Intermittent failures are a systems problem, not just a code problem. If your teamâs response to a transient error is ârestart it and see if it comes back,â youâre building up technical debt that will eventually cause a major outage. The right response is: capture the state, preserve the evidence, and start the investigation immediately. This requires tooling thatâs already in placeâyou canât add tracing after the failure occurs.
Run game days where you inject intermittent failures and practice the response. Chaos engineering isnât about breaking production; itâs about verifying that your observability stack can pinpoint the breakage. If your dashboards donât show the injected fault clearly, they wonât show the real one either.
FAQ
Whatâs the first thing I should check when an intermittent failure alert fires?
Check the deployment history. Did any service, configuration, or infrastructure change roll out in the hour before the first occurrence? Most intermittent failures in otherwise stable systems are triggered by a recent change that introduced a latent defect. If thereâs no deployment, check for external dependency changesâa third-party API that started rate-limiting, a DNS change, a certificate rotation.
How do I debug an intermittent failure that only happens once a month?
You need to capture the full state when it occurs because you wonât get another chance soon. Set up a conditional log dump triggered by the specific error signature: when the error occurs, automatically collect thread dumps, heap dumps, connection pool stats, and the last N minutes of debug logs from the affected host. Store this in a dedicated bucket with a long retention period. Next time it happens, youâll have a complete forensic snapshot.
Why do my intermittent failures disappear when I add logging?
This is a classic Heisenbug. Adding logging changes timingâthe extra I/O slows down the code path just enough to avoid a race condition, or it forces a context switch that lets a background task complete. If logging makes the bug vanish, youâre likely dealing with a race condition or a resource contention issue. Instead of adding more logging, add passive instrumentation that doesnât block: atomic counters, non-blocking ring buffers, or eBPF probes.
How can I tell if an intermittent failure is caused by my code or by infrastructure?
Correlate the failure timestamps with infrastructure metrics at the same granularity. If every failure aligns with a CPU throttling event, a network retransmit spike, or a disk I/O wait spike on the host, itâs infrastructure. If the failures occur while all infrastructure metrics are within normal bounds, itâs likely an application-level race condition or logic bug. Distributed tracing makes this correlation straightforwardâoverlay the trace timeline with host metrics.
Closing Notes
Intermittent failures are not magic. They are deterministic events triggered by a specific system state that you havenât yet observed. The difference between a team that fixes them in hours and a team that suffers for weeks is observability granularity and investigative discipline. Build your systems to surface the state you need, practice the hunt before itâs an emergency, and never accept âit went away after a restartâ as a resolution. The bug is still there, waiting for the state to align again.