Chasing Ghosts: A Field Guide to Debugging Intermittent Failures in Production

Intermittent failures are the worst kind of production bug. They don’t crash your system outright. They nibble at the edges—a timeout here, a dropped message there, a 500 error that vanishes before you can even SSH into the box. You stare at dashboards that look like a seismograph during a minor tremor, and you know something is wrong, but the logs are spotless. The metrics are within thresholds. The code, as far as you can tell, is flawless. Felix Okonkwo, a senior infrastructure engineer I’ve worked with, calls these “phantom faults,” and the name sticks because they haunt you. You can’t reproduce them in staging. Load tests don’t trigger them. Yet at 2:14 AM on a Tuesday, a user in Lagos gets a blank screen, and by 2:15 AM it’s gone.

This isn’t a mystery for intuition to solve. It’s a systems problem, and it demands a methodical, almost forensic approach. You need to think like a detective who also understands TCP retransmission timers, garbage collection pauses, and the subtle horrors of eventually consistent databases. The goal here is a concrete framework for isolating these failures—not magic, just disciplined engineering.

Step One: Define the Failure Signature

Before you touch a single log file, nail down what “intermittent” actually means in this case. Vague reports like “the site is slow sometimes” are useless. You need a signature. Is the failure tied to a specific endpoint? A time window? A user segment? A geographic region? Start by pulling raw data from your load balancers and CDN edge logs. Don’t aggregate. Look at the raw percentiles. Averages are your enemy—they smooth out the very spikes you’re hunting.

Say you’re running a Node.js service behind Nginx. Pull the $request_time for the suspect endpoint over 24 hours, bin it by minute, and look at the p99.9 latency. If you see a sawtooth pattern where the p99.9 jumps to 3 seconds every 15 minutes while the p50 stays flat at 50ms, you’ve got a signature. That periodicity is a lead. It’s not random; it’s a cycle. Now you can ask: what else in the system runs on a 15-minute cycle? Cron jobs? Cache TTL expiries? Connection pool recycling?

Step Two: Instrument the Hot Path

Once you have a signature, add targeted instrumentation. Don’t shotgun debug. Don’t sprinkle logging into every function—that creates noise and can even mask the problem by shifting timing. Instead, trace the exact request path that shows the failure. If the p99.9 spike is on /api/checkout, instrument every hop in that flow: the API gateway, the auth service, the database query, the external payment processor call. Use structured logging with explicit timing deltas between each step. A log line should look like: {"event":"checkout_db_query","duration_ms":1200,"trace_id":"abc123"}. The trace ID is non-negotiable. It lets you stitch together a single request’s journey across services.

If you don’t have distributed tracing in place, bolt it on now. Even a simple X-Request-ID header propagated through your services and logged at each boundary is better than flying blind. The intermittent failure is likely a specific combination of states—a particular user’s cart size hitting an unoptimized query, a cache miss at the exact moment a connection pool is exhausted. Without a trace, you’re staring at aggregate metrics and guessing.

Server rack with blinking lights indicating network activity

Step Three: The Resource Saturation Hypothesis

Most intermittent failures in production aren’t logic bugs. They’re resource saturation events. A logic bug is deterministic—same input, same failure, every time. An intermittent failure is usually a system pushed just past its limit, but only for a brief moment. The three horsemen here are CPU throttling, memory pressure, and I/O contention. Your job is to rule each one out systematically.

For CPU, don’t just look at overall utilization. Look at CPU steal time if you’re in a virtualized environment. A noisy neighbor on the hypervisor can steal your vCPU cycles for milliseconds at a time, causing timeouts in your event loop. Run top and check the %st column. If it’s non-zero during your failure windows, you’ve got a suspect. For memory, the killer is often not a leak but a GC pause. If you’re running a managed runtime like the JVM or Node.js, enable GC logging with timestamps. Correlate GC pause times with your latency spikes. A 200ms stop-the-world pause in a service that normally responds in 50ms will cause a wave of timeouts. For I/O, check disk queue depth and network socket buffer overflows. A sudden spike in disk writes from a background compaction job can starve your database of IOPS for a few seconds.

Step Four: The Network Is Not Reliable

Engineers often treat the network as a black box that either works or doesn’t. It doesn’t. It degrades. It drops packets. It reorders them. It introduces jitter. Intermittent failures are frequently network-induced, especially in distributed systems that rely on fast consensus or heartbeats. If you’re using a connection pool to a database, check for TCP retransmissions. A single retransmission can add 200ms to a query. Run ss -ti on the client host and look for retrans and rto values. If you see retransmissions climbing during your failure windows, the network is your culprit.

Also, inspect your load balancer’s health check configuration. A common failure pattern: a backend server is marked unhealthy due to a brief GC pause or CPU spike, the load balancer removes it from the pool, and the remaining servers get a sudden traffic surge that pushes them into saturation. By the time the original server recovers and passes health checks, the damage is done. The failure is intermittent because it only happens when the health check interval aligns with the resource spike. Tighten your health check thresholds or add a grace period for re-entry.

Close-up of network cables and server ports

Step Five: Reproduce by Amplifying the Stressor

You can’t wait for the failure to happen again. You need to force it. This is where chaos engineering principles apply, but in a targeted way. You’re not randomly killing pods; you’re amplifying the specific condition you suspect. If you think the issue is connection pool exhaustion, reduce the pool size in a staging environment that mirrors production traffic patterns. If you suspect GC pauses, allocate less heap memory to the service. If you suspect a race condition in a database transaction, increase the concurrency of that specific operation by a factor of 10.

The key is to isolate the stressor. Don’t change five variables at once. Change one, and observe. If the failure rate increases, you’ve found your lever. Then you can work backward to the root cause. This is often a configuration default that was never tuned for your actual workload—a database connection timeout set to 30 seconds when your upstream load balancer times out at 10 seconds, a thread pool sized for peak load but not for peak load plus a cache flush.

Step Six: Correlate with Deployment Events

Intermittent failures often appear after a deployment, but not immediately. They can take hours or days to manifest as caches warm up, connection pools stabilize, and traffic patterns shift. Pull your deployment timeline and overlay it with the failure signature. Look for a “gray failure”—a partial degradation that doesn’t trigger your monitoring alerts but slowly erodes performance. A new feature might introduce a slightly slower database query that, under normal load, is fine. But when combined with a background job that runs every hour, it pushes a critical resource over the edge. The failure is intermittent because the background job is intermittent.

If you find a correlation, don’t just roll back the deployment. That fixes the symptom but leaves you ignorant. Instead, diff the performance characteristics of the old and new code paths. Profile the new query under production-like data volumes. You’ll often find a missing index, an N+1 query, or a serialization change that bloats payload sizes.

Step Seven: Observability Over Monitoring

Monitoring tells you something is wrong. Observability lets you ask arbitrary questions about your system without deploying new code. If you’re debugging intermittent failures, you need high-cardinality observability. That means being able to slice and dice your telemetry by user ID, session ID, request ID, server instance, and software version. Aggregate metrics like p99 latency are a starting point, not an endpoint. You need to be able to ask: “Show me the latency distribution for requests from users in Nigeria that hit server instance i-0a1b2c3d between 02:13 and 02:15 UTC, grouped by database query type.” If your observability stack can’t answer that, you’re blind.

This is where tools like Honeycomb or a well-instrumented Grafana Loki setup earn their keep. You’re not looking for a needle in a haystack; you’re looking for a specific needle in a stack of needles. High-cardinality fields are the magnet. Add custom attributes to your spans: user tier, feature flags, cache hit/miss status, downstream service version. When the failure occurs, you can group by these dimensions and spot the pattern. Maybe all failures are for users with a specific feature flag enabled. Maybe they all hit a stale cache node. Without these dimensions, you’re just staring at a p99 spike with no leads.

Step Eight: The Blame-Free Postmortem

Once you’ve identified the root cause, document it. Not to assign blame, but to build institutional knowledge. Intermittent failures are often systemic—they reveal a flaw in the architecture, not a mistake by an individual. The postmortem should answer: what was the failure signature? What was the root cause? How did we detect it? How did we mitigate it? And most importantly, what prevents this class of failure from happening again? That last question is where the real engineering happens. It might mean adding a circuit breaker, adjusting a timeout, or implementing a backpressure mechanism. It might mean rewriting a query to be constant-time instead of linear. Whatever it is, make it a concrete action item with an owner and a deadline.

Intermittent failures are not acts of God. They are emergent behaviors of complex systems. They can be understood, reproduced, and eliminated. But only if you treat them as engineering problems, not mysteries. Stop rebooting servers and hoping. Start measuring, tracing, and reasoning from first principles. The ghost in the machine is just a process you haven’t instrumented yet.

Engineer analyzing server logs on multiple monitors

FAQ

Why do intermittent failures often happen at night?

Nighttime failures are frequently caused by automated maintenance jobs—database backups, log rotation, index rebuilds, or batch processing—that compete for I/O, CPU, or memory. These jobs are scheduled during low-traffic periods, but they can still saturate resources and cause timeouts for the few requests that do arrive. Check your cron schedules and job durations against the failure timestamps.

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

You can’t reproduce it in the traditional sense, but you can amplify the suspected stressor. If you suspect a race condition under high concurrency, use a load-testing tool to hammer that specific endpoint with 10x normal traffic in a staging environment. If you suspect a slow database query under a specific data pattern, seed the staging database with that pattern and run the query. The goal is to make the intermittent failure deterministic by creating the worst-case scenario.

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

A Heisenbug is a specific type of intermittent failure that changes or disappears when you try to observe it, often due to timing alterations from added logging or debuggers. True intermittent failures are broader—they may be consistently intermittent regardless of observation. Heisenbugs are usually caused by race conditions or memory corruption. If adding logging makes the failure vanish, you’re likely dealing with a Heisenbug, and you need to use non-invasive tracing like eBPF or passive network taps.

Hunting Ghosts in Production: A Systems Engineer’s Guide to Intermittent Failures

Intermittent Failures Are a Different Beast

Intermittent failures in production are the worst kind of problem. They don’t announce themselves. They don’t leave a neat stack trace you can grep for. They flicker in and out of existence, often tied to a specific confluence of load, timing, and state that you can’t easily recreate. If you’re the one on call, you know the drill: a spike in 500s, a flurry of confused Slack messages, and then—silence. The system heals itself before you can grab a thread dump. Hard crashes are almost comforting by comparison. At least they’re consistent.

This isn’t a problem you solve with intuition. It’s a problem you solve by turning the system inside out, instrumenting every layer, and refusing to trust anything you haven’t measured. The approach is part detective work, part experimental physics. You’re not just looking for a broken line of code. You’re looking for the exact set of conditions that make a working system suddenly fail.

Server rack with blinking lights indicating network activity

Collect the Evidence Before You Form a Theory

When an alert fires, the natural impulse is to blame the last deployment or the database that’s been acting up. Fight that impulse. Intermittent failures are rarely monocausal. They’re emergent—a combination of factors that align just long enough to break something. If you jump to a conclusion, you’ll waste time chasing a ghost while the real trigger remains hidden. Start with the raw facts.

Define the failure in concrete terms. What exactly did the user see? A blank page? A timeout? A garbled response? Capture the timestamp, the affected endpoint, the user ID, the session token. If it’s a frontend issue, grab the browser console logs and the network waterfall for that session. If it’s a backend service, pull the request ID and trace it through every hop in your logging pipeline. You’re not just looking for the error. You’re reconstructing the sequence of events that led up to it. The failure itself is just the final frame of a bad movie.

You Can’t Debug What You Can’t See

If your production system isn’t instrumented, you’re debugging with a blindfold on. Structured logging is the bare minimum—every log line needs consistent fields: request ID, service name, latency, status code, user ID. Without those, you’re just grepping through unstructured text and hoping for a miracle. Metrics need to go beyond CPU and memory. You need application-level signals: thread pool saturation, connection pool wait times, queue depths, GC pause durations. These are the vital signs of a living system, and intermittent failures often show up as anomalies in these metrics long before they become user-facing errors.

Distributed tracing is non-negotiable if you’re running more than two services. A single request might touch half a dozen components. Without a trace, correlating a spike in Service D’s latency with a timeout in Service A is a guessing game. With a trace, it’s a five-second query. For intermittent failures, high-cardinality metrics are your best friend. Averages lie. p95 and p99 latencies tell the truth. Histograms can reveal bimodal distributions—a sure sign that two different code paths or resource pools are in play.

Correlation Is a Clue, Not a Conviction

Once you have a detailed timeline of the failure, start overlaying system-wide metrics. Look for anything that changed at the same time. Did CPU spike on one host? Did a cron job fire? Was there a network partition? Did a downstream service start returning slow responses? Dashboards with aligned time-series panels make this kind of visual correlation fast. If you don’t have one, build a dedicated incident-correlation dashboard that pulls in data from load balancers, app servers, databases, caches, and external dependencies.

Pay attention to leading indicators. A metric that shifts before the failure is far more interesting than one that shifts at the same time or after. A gradual climb in connection pool wait times that peaks right as errors appear? That’s a smoking gun for pool exhaustion. A sudden drop in cache hit rate followed by a spike in database latency? You’re probably looking at a cache failure or an eviction storm. The timeline matters as much as the values.

Reproduce the Conditions, Not Just the Request

You can’t reproduce an intermittent failure by replaying a single request in isolation. The bug depends on state—memory pressure, connection pool saturation, the timing of a competing thread. You need to recreate the environment that made the failure possible. That usually means load testing with realistic traffic patterns, injecting latency into dependencies, or squeezing resource limits.

If the failure correlates with high memory usage, run a canary instance with a smaller heap and watch the error rate. If it correlates with a slow upstream service, use Toxiproxy to inject artificial latency and see if the same failure mode appears. The point is to design a controlled experiment that triggers the failure on demand. Until you can do that, you don’t really understand the bug. You just have a hunch.

Close-up of network cables and server indicators

Get Your Hands Dirty in the Code

Once you have a strong correlation, it’s time to read the code paths involved. Look for shared mutable state, anything that isn’t thread-safe, and race conditions. Intermittent failures love concurrency bugs. They hide in lazy caching, stale data reads, and assumptions about the order of async operations. If the failure involves a timeout, trace every hop. A common pattern: Service A calls Service B with a 5-second timeout. Service B calls Service C with a 4-second timeout. Under normal load, everything finishes in 2 seconds. But when Service C slows down, Service B’s timeout fires at 4 seconds while Service A is still waiting. Service A gets a partial response or a generic error, and the real culprit—Service C—is completely invisible. Distributed tracing with span annotations makes this obvious in seconds.

Your Resilience Code Might Be the Problem

Here’s an uncomfortable truth: the code you wrote to make the system reliable often causes intermittent failures. Retry storms are the classic example. A request fails, triggers a retry, that retry fails, triggers more retries, and suddenly an already struggling service is buried under a self-inflicted DDoS. Circuit breakers that trip too eagerly can turn a transient blip into a hard failure. Fallback logic that returns stale or incomplete data can confuse downstream systems in ways that cascade. Audit your resilience patterns. Retries need exponential backoff and jitter—no exceptions. Circuit breakers need sane thresholds and reset timers. And your logging needs to capture every retry attempt: the original failure reason, the retry count, and the final outcome. Otherwise, you’ll see a success in the metrics and never know it took three attempts and nearly timed out.

Use Production Traffic as a Lab

Sometimes the only way to catch an intermittent bug is to watch it happen in production. That doesn’t mean attaching a debugger to a live server—please don’t do that. It means safely sampling or mirroring real traffic. Traffic shadowing copies a percentage of requests to a test instance that runs the same code but doesn’t touch real users. If the shadow instance logs the same errors, you can experiment with fixes without risking production. Another technique is incremental rollout with feature flags. If you suspect a recent code change, use a flag to shift traffic gradually—1%, then 5%, then 25%—while watching error rates. If the error rate tracks the rollout percentage, you’ve found your culprit. Roll back immediately and dissect the diff.

When the Bug Lives in the Infrastructure

Not every intermittent failure is a code bug. Hardware gets flaky. A switch drops packets under load. A disk develops slow sectors that cause I/O latency spikes. Cloud instances suffer from noisy neighbors that steal CPU or network bandwidth. These are harder to diagnose because you don’t control the physical layer. But you can still detect them by watching system-level metrics: CPU steal time, disk I/O await, network retransmits. If you see spikes that correlate with application errors, consider migrating workloads or enabling redundancy.

DNS is a silent killer. A stale DNS cache can route traffic to a decommissioned server. An intermittent resolution failure can cause random connection errors. Check your DNS TTLs and make sure your resolvers are healthy. Log DNS resolution times and errors at the application level. Don’t assume the infrastructure team has it covered.

Engineer analyzing server logs on multiple monitors

Make Debugging a Team Sport

Debugging intermittent failures isn’t a solo activity. It requires a team that values observability and runs blameless postmortems. When an incident happens, document everything: the timeline, the hypotheses you tested, the data that confirmed or refuted them, and the final root cause. Share it with the team so everyone learns. Over time, you’ll build a playbook of common failure patterns and their signatures. Future investigations will go faster because you’ve seen the same movie before.

Invest in chaos engineering, but be deliberate about it. Start by injecting failures into a staging environment that mirrors production. Then, gradually move to production during low-traffic periods. The goal is to expose weaknesses before they become customer-facing incidents. If your system can’t handle a controlled experiment, it definitely can’t handle a real failure.

FAQ

Why do intermittent failures often correlate with deployments?

Deployments change the system’s state—new code, updated configs, restarted services. These changes can expose latent race conditions, shift timing assumptions, or increase resource usage just enough to push a component over its limit. The failure may not appear immediately because it needs a specific traffic pattern or a cumulative effect, like a slow memory leak, to trigger it.

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

Set up long-term logging and metric retention. For rare events, you need weeks or months of data to spot patterns. Use anomaly detection on key metrics to automatically flag deviations. When the failure occurs, capture a full snapshot: thread dumps, heap dumps, network connection states, and recent request logs. Treat it like capturing a rare animal—you need traps set before it appears.

What’s the first thing to check when a service starts timing out intermittently?

Check connection pool utilization and wait times. Most intermittent timeouts come from exhausted connection pools—either to databases, caches, or downstream services. Look at the pool’s active connections, idle connections, and pending waiters. If waiters are queuing up, find out why connections aren’t being released. It’s often a slow query, a network blip, or a client that’s not closing connections properly.

Debugging Intermittent Failures in Production Systems: A No-Nonsense Guide

Why Intermittent Failures Are the Worst Kind of Bug

Intermittent failures in production are the bane of any engineer’s existence. They don’t reproduce on demand. They mock your unit tests. They vanish the moment you attach a debugger. And yet, they can erode user trust, corrupt data, and wake you up at 3 a.m. with a PagerDuty alert that clears itself before you even log in. Felix Okonkwo here, and I’ve spent enough years staring at distributed traces and kernel logs to know that these bugs aren’t just annoying—they’re a sign that your system has hidden complexity you don’t fully understand. This article cuts through the noise. No fluff. Just the technical patterns, tools, and mindsets you need to hunt down transient failures in live environments.

We’ll cover the common root causes, the instrumentation you should already have in place, and a structured approach to investigation that doesn’t rely on luck. If you’re looking for a silver bullet, stop reading. If you want a framework that works, keep going.

Understanding the Nature of Intermittent Failures

An intermittent failure is one that occurs unpredictably, often under conditions that seem identical to those where the system works perfectly. These failures are almost never caused by a single, deterministic bug in application logic. Instead, they emerge from the interaction of multiple components under specific, transient conditions. Think race conditions, resource exhaustion, network packet loss, cosmic-ray-induced bit flips, or garbage collection pauses hitting a timeout threshold.

The first step in debugging is accepting that you cannot rely on reproduction in a staging environment. You must instrument production to capture the failure’s context at the moment it occurs. This means logging, metrics, and distributed tracing must already be in place—retrofitting them after an incident is like trying to install a black box on a plane that’s already crashed.

Common Root Causes

Intermittent failures typically fall into a few categories. Recognizing the category narrows your search space dramatically.

  • Resource Saturation: CPU throttling, memory pressure, file descriptor exhaustion, or thread pool starvation. These often manifest as timeout errors or slow responses that trigger upstream retries, compounding the problem.
  • Concurrency Bugs: Race conditions, deadlocks, or livelocks in multi-threaded code. Symptoms include corrupted shared state, operations applied out of order, or requests hanging indefinitely.
  • Network Instability: Packet loss, DNS resolution failures, or load balancer health check flaps. TCP retransmissions can cause latency spikes that breach client-side timeouts.
  • External Dependency Failures: Third-party APIs returning transient errors, database connection pool exhaustion, or message queue backpressure. Your system’s error handling for these dependencies is often the real culprit.
  • Hardware/Infrastructure Glitches: Noisy neighbors in virtualized environments, disk I/O contention, or faulty RAM. These are rare but devastating when they occur.

Instrumentation: The Non-Negotiable Foundation

You cannot debug what you cannot see. If your production system lacks observability, you’re flying blind. Here’s the minimum you need before an intermittent failure strikes.

Structured Logging with Correlation IDs

Every request must carry a unique identifier—a correlation ID—that propagates across service boundaries. This ID must appear in every log line, every error message, and every trace span. Without it, reconstructing the chain of events for a single failed request is impossible. Use a format like JSON for logs so you can query fields directly rather than grepping through unstructured text.

Log at the boundaries: incoming requests, outgoing calls to dependencies, and error paths. Include timestamps with millisecond precision. Log the request payload (sanitized) and the response status. When a failure occurs, you need to know exactly what was sent and what came back.

Metrics with High Cardinality

Aggregate metrics (request rate, error rate, latency percentiles) tell you that something is wrong. They don’t tell you why. For intermittent failures, you need metrics broken down by dimensions that matter: per endpoint, per error type, per dependency, per instance. A spike in p99 latency on a single pod that correlates with a memory pressure metric is a smoking gun. Without those dimensions, you just see noise.

Track queue depths, thread pool utilization, connection pool wait times, and garbage collection pause durations. These are leading indicators. A thread pool approaching saturation will cause intermittent timeouts long before it causes a hard failure.

Distributed Tracing

Traces are your timeline for a single request as it hops across services. They show you where time was spent, which calls failed, and what the error was. For intermittent failures, compare a failed trace with a successful one for the same endpoint. Look for differences in the services called, the order of calls, or the timing. A 2ms delay in acquiring a database connection can cascade into a 500ms timeout three services downstream.

Structured Investigation Process

When an intermittent failure alert fires, don’t panic. Follow a disciplined process. Randomly restarting services or rolling back deployments without evidence is cargo-cult debugging. It might work by accident, but you’ll never learn the root cause.

Step 1: Define the Failure Signature

Start with the alert. What exactly failed? A 5xx response? A timeout? A null value where data was expected? Capture the exact error message, the endpoint, the time window, and the affected users or requests. If you have a trace ID or correlation ID, grab it immediately. This is your anchor.

Step 2: Examine the Request Path

Pull the trace for the failed request. Identify every service, database query, and external call in the path. Note the latency at each step. If the trace is incomplete (e.g., missing spans), that’s a clue: something crashed or timed out before writing the span. Check logs for that service around the failure timestamp, filtering by correlation ID.

Step 3: Check Resource Utilization at Failure Time

Overlay the failure timestamp on your infrastructure metrics. Look for CPU throttling, memory spikes, GC pauses, or network errors. If the failure correlates with a CPU throttle event, the service was likely starved and couldn’t process the request in time. If it correlates with a memory spike, the service may have been pausing for GC. Container orchestration platforms like Kubernetes expose these metrics; use them.

Step 4: Analyze Dependency Health

Check the health of every downstream dependency at the failure time. Did the database experience a replication lag? Did the cache have an eviction spike? Did a third-party API return errors? Intermittent failures are often caused by dependencies that are themselves experiencing transient issues. Your service’s error handling and retry logic can amplify these into visible failures.

Step 5: Reproduce the Conditions, Not the Bug

You may never reproduce the exact bug. Instead, reproduce the conditions that led to it. If the failure occurred during a CPU throttle, inject CPU stress into a canary instance and observe behavior. If it happened when a dependency was slow, use fault injection to add latency to that dependency. Chaos engineering isn’t about breaking things randomly; it’s about testing your system’s resilience to known failure modes.

Case Study: The Vanishing Timeout

Let’s walk through a real-world example. A payment processing service experienced intermittent 504 Gateway Timeout errors. The errors occurred roughly once per hour, affecting about 0.1% of requests. The service called an external payment gateway with a 5-second timeout. Logs showed the external call completing in under 2 seconds, yet the client received a 504 after 5 seconds. The trace showed the full 5 seconds spent waiting for the external call, but the external gateway’s logs showed a 1.8-second response time. Where did the other 3.2 seconds go?

Investigation revealed that the service used a shared HTTP client connection pool with a maximum of 50 connections. Under normal load, 30 connections were in use. However, periodic bursts from a batch job consumed all 50 connections. Requests arriving during these bursts waited in a connection request queue with a default timeout of 5 seconds. The external call itself was fast, but acquiring a connection took 3.2 seconds. The total time exceeded the client’s timeout, resulting in a 504. The fix was increasing the connection pool size and adding a dedicated pool for the batch job.

This bug was intermittent because the batch job ran periodically. The connection pool saturation was invisible in standard metrics because they only measured active connections, not queue wait time. Adding a metric for connection acquisition latency made the problem immediately obvious.

Tools That Actually Help

Some tools are overhyped; others are indispensable. Here’s what I reach for when hunting intermittent failures.

  • Distributed Tracing: Jaeger or Zipkin. If you’re on a cloud provider, their native tracing solution (AWS X-Ray, Google Cloud Trace) works. The key is sampling. You need head-based sampling that captures all traces for requests that meet certain criteria (e.g., latency > 1s, error status code). Tail-based sampling is better but harder to implement. At minimum, log trace IDs so you can find the full trace later.
  • Metrics and Dashboards: Prometheus with Grafana. Set up dashboards that show request rate, error rate, and latency percentiles broken down by endpoint and instance. Add panels for thread pool utilization, connection pool wait times, and GC metrics. When an alert fires, these dashboards are your first stop.
  • Log Aggregation: Elasticsearch, Loki, or your cloud provider’s log service. You need full-text search with field-level queries. The ability to query correlation_id:abc123 AND level:ERROR across all services is non-negotiable.
  • Profiling: Continuous profiling tools like Pyroscope or Google Cloud Profiler. They show you what your code is actually doing in production—CPU usage, memory allocation, lock contention. For intermittent latency spikes, a profiler can reveal that a normally fast code path occasionally hits a slow branch due to specific input data.

Code-Level Patterns That Cause Intermittent Failures

Sometimes the bug is in your code, hiding in plain sight. These patterns are frequent offenders.

Unbounded Queues and Buffers

An in-memory queue that grows without limit will eventually cause GC pressure, memory exhaustion, or multi-second processing delays. Always bound your queues. Use backpressure to slow down producers when consumers fall behind. An unbounded queue is a ticking time bomb.

Implicit Assumptions About Ordering

In distributed systems, messages can arrive out of order, be delivered more than once, or be delayed by minutes. Code that assumes FIFO ordering or exactly-once delivery will fail intermittently when these assumptions are violated. Design for at-least-once delivery with idempotent processing.

Timeouts Without Jitter

If every retry happens at exactly the same interval, thundering herds can form. A service that fails and causes all clients to retry simultaneously after a fixed 1-second timeout will be hit with a spike of requests exactly 1 second later. Add random jitter to retry intervals to spread the load.

Shared Mutable State Without Synchronization

This is basic, but it still happens. A cache implemented as a plain HashMap accessed by multiple threads will eventually corrupt itself. Use concurrent data structures or proper locking. The failure will be rare—maybe once a week—but it will cause bizarre, unreproducible errors.

Testing Strategies for Intermittent Failures

You can’t rely on production to be your only test environment. Shift-left your resilience testing.

Chaos Engineering in Staging

Before code reaches production, run chaos experiments against it in a staging environment that mirrors production topology. Inject latency, kill pods, partition the network. Use a tool like Chaos Mesh or LitmusChaos. Start with small blast radius experiments and gradually increase scope. The goal is to discover intermittent failure modes before your users do.

Deterministic Simulation Testing

For complex distributed algorithms, use a deterministic simulator like FoundationDB’s simulation framework or Jepsen. These tools control the entire system clock and network, allowing you to inject specific faults and replay the exact sequence of events that triggered a bug. This is the gold standard for concurrency bugs, but it requires building your system with simulation in mind.

Property-Based Testing

Instead of writing specific test cases, define properties that must always hold. A property might be “for any sequence of concurrent deposits and withdrawals, the final balance must equal the sum of deposits minus withdrawals.” A property-based testing library (QuickCheck, Hypothesis) generates random sequences of operations and checks the property. It will find the minimal failing sequence, which often reveals a race condition.

When You Can’t Find the Root Cause

Sometimes, despite all your instrumentation and analysis, the root cause remains elusive. The failure is too rare, the data too sparse. In these cases, you have options beyond giving up.

First, increase your sampling rate. If you’re sampling 1% of traces, bump it to 10% or 100% temporarily. The performance overhead is usually acceptable for short periods. More data means more chances to catch the failure in the act.

Second, add targeted logging. Identify the code path that’s most likely involved and add debug-level logging with the specific variables you need. Use feature flags to enable this logging dynamically without redeploying. When the failure recurs, you’ll have the data.

Third, implement a circuit breaker or retry policy with exponential backoff and jitter. Even if you don’t find the root cause, you can make the system resilient to the failure. This is a pragmatic stopgap, not a solution—but it keeps users happy while you continue investigating.

Building a Culture of Debugging Rigor

Intermittent failures are a team problem, not an individual one. Your organization must support the practices that make debugging possible. This means treating observability as a feature, not an afterthought. It means blameless postmortems where the focus is on systemic improvements, not assigning fault. It means giving engineers time to investigate properly rather than pressuring them to “just restart the service and move on.”

Every intermittent failure that goes unresolved is a debt that will be paid with interest in future incidents. Invest in the tooling and processes now, or pay the cost in downtime later.

FAQ: Intermittent Failures in Production

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

This is the observer effect in action. Attaching a debugger, enabling verbose logging, or increasing metrics collection changes the timing and resource usage of your application. A race condition that depends on nanosecond-level timing will almost never occur under a debugger because the debugger slows everything down. Instead of interactive debugging, rely on passive instrumentation like distributed tracing and structured logging that don’t alter execution timing significantly.

How do I convince management to invest in observability for intermittent failures?

Translate the problem into money. Calculate the cost of downtime per hour, the engineering hours spent on war rooms, and the customer churn from unreliable service. Then compare it to the cost of implementing proper tracing, logging, and metrics. A single major incident often costs more than a year of observability tooling. Present a business case, not a technical wishlist.

What’s the most overlooked cause of intermittent timeouts?

DNS resolution. Many services resolve hostnames on every request or cache resolutions with a short TTL. A slow DNS server can add hundreds of milliseconds to every request, intermittently pushing total latency over the timeout threshold. Monitor DNS resolution time as a metric. Use a local caching resolver like dnsmasq or configure your application’s DNS cache appropriately.

Can intermittent failures be caused by client-side issues?

Absolutely. Mobile apps on unreliable cellular networks, browsers with flaky internet connections, or IoT devices with poor signal strength can all produce errors that look like server-side failures. Always check the client’s network conditions. Include client-side telemetry in your observability stack—request latency measured from the client, network type, and signal strength. A server-side 504 might actually be a client-side timeout due to a slow network.

Final Thoughts

Debugging intermittent failures in production is a skill that separates senior engineers from juniors. It requires systems thinking, a methodical approach, and the humility to accept that your code doesn’t run in a vacuum. It runs on real hardware, over real networks, alongside noisy neighbors, under conditions you didn’t anticipate. Build your systems to be observable, test them under stress, and when a failure occurs, follow the evidence—not your intuition. The bug is always logical. Your job is to find the logic.

Server rack with blinking lights indicating network activityEngineer analyzing data on multiple monitors in a dark roomClose-up of network cables connected to a switch

Debugging Intermittent Failures in Production Systems: A No-Nonsense Guide

The Phantom in the Machine

Intermittent failures are the worst kind of production bug. They don’t blow the system up. They flicker. A request fails once every thousand calls. A database query times out, but only when traffic spikes. A message queue consumer drops a message, and it’s always on a Tuesday. These failures mock your dashboards and laugh at your unit tests. I’ve lost count of the nights I’ve stared at logs that look pristine, knowing something is rotting just out of sight. This article is a straight-up, technical walkthrough of how I track these phantoms down.

Start with the Signal, Not the Noise

Your first instinct will be to grep the logs for “error.” Don’t. That’s a trap. Intermittent failures rarely leave a neat error message. They show up as latency spikes, null returns, or operations that silently vanish. You need to define the failure by its observable symptoms, not by log levels. A 500 status code is a symptom. A 200 that’s missing a required field? Also a symptom. A 200 that took 4.2 seconds when your p99 is 200ms? That’s a screaming symptom.

I start by writing a tight query against my metrics system, not the log aggregator. In Datadog or Prometheus, I zoom in on the exact time window of the reported incident. I pull p99.9 latency, error rate (even if it’s 0.1%), and request volume. Volume is the linchpin. Intermittent failures often sync up with traffic shape. A slow memory leak only triggers GC pauses when the heap is nearly full, which happens at peak traffic. Connection pool exhaustion only hits when the arrival rate outpaces the service time for a sustained period. Without the volume metric, you’re flying blind.

Server rack with blinking lights indicating activity

Correlation is Your First Real Lead

Once you have a time series of the failure, start overlaying other metrics. Did a deployment happen in the last hour? Check the CI/CD pipeline timestamps. Did a dependent service hiccup? Pull its latency and error graphs. Did a cron job or batch process kick off? Those things are notorious for hogging shared resources like CPU, I/O, or database connections.

I once debugged a “random” 504 gateway timeout that hit exactly every 90 minutes. It wasn’t random. A misconfigured health check on a downstream service was triggering a rolling restart of its containers. The restart took 45 seconds, during which the load balancer had zero healthy targets. The fix wasn’t in my service; it was in the downstream deployment config. The logs showed nothing but timeouts. The metrics showed a periodic dip in healthy host count. That’s the difference.

Instrument the Code Like a Surgeon

If your existing metrics and logs don’t expose the cause, you need to add targeted instrumentation. Don’t just scatter log lines everywhere. That adds noise and can even make the problem worse by increasing I/O pressure. Instead, wrap the suspect code path with a timer and a counter. In Java, a simple Timer.Context from Dropwizard Metrics around the method in question is worth more than a thousand printf statements. In Python, a decorator that records duration and exception type to StatsD. The goal is to answer two questions: How often does this code path actually fail? And when it fails, how long did it take?

For stateful bugs, you need to capture the state at the moment of failure. A connection pool exhaustion bug requires knowing the active, idle, and pending connection counts right when it blows. A race condition requires capturing the sequence of events across threads. This is where structured logging with a trace ID becomes non-negotiable. Every request must carry a unique identifier that propagates across service boundaries. Without it, you can’t reconstruct the chain of events for a single failed request.

Close-up of network cables plugged into a switch

Reproduce or Die Trying

You can’t fix what you can’t reproduce. But reproducing intermittent failures in production is asking for trouble. Reproducing them in staging is often impossible because the traffic patterns and data entropy are different. My approach: shadow traffic replay. I take a sample of real production requests—anonymized if necessary—and replay them against a dedicated test cluster that mirrors production hardware and configuration. Then I ramp up concurrency until the failure appears. This isn’t a unit test. It’s a stress test with real-world data messiness.

If shadow replay isn’t an option, I use chaos engineering in a pre-production environment. I inject latency, drop packets, or kill dependencies to simulate the conditions that correlate with the failure. The trick is to be systematic. Change one variable at a time. If the failure correlates with a specific downstream latency profile, inject that exact latency distribution and see if the failure rate matches.

Common Culprits and Their Signatures

After years of this work, I’ve found that intermittent failures mostly fall into a few buckets. Recognizing the signature speeds up the diagnosis.

Resource Exhaustion

Thread pools, connection pools, file descriptors, memory. The failure rate climbs with traffic volume and recovers after a lull. The system doesn’t crash; it just starts refusing work or timing out. The fix usually involves tuning pool sizes, adding circuit breakers, or plugging leaks. A thread dump taken during the failure window is gold. You’ll see threads blocked on getConnection() or Semaphore.acquire().

Race Conditions

These are the hardest. The failure is truly random, with no correlation to traffic volume. It usually involves shared mutable state without proper synchronization. A cache that’s updated and read concurrently. A counter that isn’t atomic. The signature is a low, constant error rate that doesn’t budge with load. To find it, review the code for any shared state accessed outside a lock or transactional boundary. Static analysis tools can help, but nothing beats a careful code review by someone who understands the Java Memory Model—or the equivalent for your language.

Time and Ordering Assumptions

Systems assume clocks are monotonic and messages arrive in order. They aren’t, and they don’t. NTP adjustments can make a timestamp appear to jump backward, breaking any logic that depends on timestamp > lastTimestamp. Distributed queues can deliver messages out of order under partition conditions. If your failure involves data that looks “stale” or events processed in the wrong sequence, question every assumption about time and order.

Rows of servers in a data center

Build a Hypothesis and Attack It

Once you have a suspect, don’t just slap on a fix and walk away. You need to prove the hypothesis. If you think it’s a connection pool leak, add a metric that tracks pool usage over time and check if it trends upward until exhaustion. If you think it’s a race condition, write a test that hammers the critical section with hundreds of concurrent threads and checks for invariants. If you think it’s a garbage collection pause, enable GC logging with timestamps and correlate the pause times with the latency spikes.

I once suspected a “random” timeout was caused by a DNS resolution delay. The app’s HTTP client had a default connect timeout of 2 seconds, but the DNS resolver was configured with a 5-second timeout. Under rare conditions, the DNS query would hang, and the connect timeout would fire first, masking the real problem. I proved it by adding a metric for DNS resolution time. The spikes lined up perfectly. The fix was to set an explicit DNS timeout shorter than the connect timeout and to add local caching.

When the Bug is in the Infrastructure

Sometimes the problem isn’t in your code. It’s in the kernel, the container runtime, the load balancer, or the cloud provider’s network. These are the most infuriating because you have limited visibility. You need to gather evidence from the boundary. Capture packet traces on the host during the failure window. Look for TCP retransmissions, duplicate ACKs, or connection resets. Check the system logs for OOM killer events or disk I/O errors. If you’re on a cloud platform, file a support ticket with precise timestamps and request their internal metrics for that window. Be the squeaky wheel.

FAQ

Why do intermittent failures often happen at peak traffic?

Peak traffic stresses resource limits. Connection pools hit their max, thread pools queue up, memory usage approaches the heap limit triggering frequent GC pauses, and CPU contention slows down request processing. These conditions expose latent defects that are invisible under low load. The failure isn’t caused by the traffic itself, but by the resource saturation that traffic induces.

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

You need persistent, high-resolution metrics. Set up a dashboard that tracks the failure rate over a rolling 7-day window with hourly granularity. Log every occurrence with a full stack trace and request context. When the failure happens, immediately snapshot the state of the system: thread dumps, heap dumps, connection pool stats, and dependent service metrics. Treat it like a crime scene. If you can’t capture it in real time, configure alerts to trigger automatic diagnostics collection.

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

Check for recent changes. Deployments, configuration updates, feature flags, DNS changes, certificate rotations, and dependency version bumps are the most common triggers. Even a minor change in a seemingly unrelated service can cause cascading effects. Pull the change log for the last 24 hours and correlate each change with the onset of the failure. If you can roll back a change and the failure disappears, you have your root cause.

Debugging Intermittent Failures in Production: A No-Nonsense Guide

Intermittent failures in production are the absolute worst. They don’t show up on your machine. They don’t appear in staging. They strike at 3 a.m., spike your error rate for ten minutes, then disappear without a trace. You’re left with a cryptic log line and a support ticket from a user who can’t reproduce the issue. If you’re Felix Okonkwo, you don’t wait for a pattern to emerge—you hunt it down with methodical, technical aggression.

This article cuts the fluff. We’ll walk through the root causes, the tools, and the mindset you need to catch these ghosts. No hand-holding, no theory without practice. Just the hard-won techniques that work when your pager goes off.

Why Intermittent Bugs Are Different

A deterministic bug is a logic error: given input X, you always get wrong output Y. Fix the code, deploy, done. Intermittent failures are probabilistic. They depend on state that’s hard to observe or control—timing quirks, resource exhaustion, network hiccups, even cosmic rays flipping a bit in RAM (rare, but real). Your unit tests won’t catch them because they don’t replicate the messy reality of production.

So stop staring at the code. You need data from the battlefield.

Step 1: Instrument Before You Need It

If you’re debugging an intermittent failure right now and your observability is garbage, you’re already behind. But you can still claw your way out. Add targeted instrumentation without a full redeploy if possible—feature flags, dynamic log levels, or attaching a debugger to a live process (carefully).

For the long game, your production system must emit structured logs, metrics, and traces. Not “maybe later.” Now. Every HTTP request, database query, and RPC call should be wrapped in a trace span. Logs must include correlation IDs. Metrics should track not just p99 latency but also the distribution of error codes, queue depths, and connection pool utilization. Without this, you’re blind.

Structured logging means JSON or something similar, not free-text strings. You need to query logs by fields: status_code:500 AND endpoint:/api/checkout. If you’re grepping plain text logs at 2 a.m., you’ve already lost.

Distributed tracing is non-negotiable for microservices. An intermittent timeout in Service A might be caused by a garbage collection pause in Service D. Without trace context propagating across calls, you’ll never connect the dots. Use OpenTelemetry. Instrument your HTTP client libraries, your database drivers, your message queues. If a library doesn’t support it, wrap it yourself.

Server rack with blinking lights

Step 2: Characterize the Failure Signature

Before you touch any code, define the problem precisely. Intermittent failures often get lumped together as “flaky,” but that’s lazy. Break it down:

  • Frequency: How often does it occur? 0.1% of requests? 5% during peak traffic? Only on Tuesdays?
  • Duration: Does it self-resolve in seconds, or persist until a restart?
  • Affected components: Is it one service, one endpoint, one database node, or everything behind a specific load balancer?
  • User impact: Timeouts? 500 errors? Stale data? Silent data corruption?
  • Correlated events: Deployments, config changes, traffic spikes, cron jobs, cloud provider maintenance windows.

Plot error rates against time, traffic, and deployment events. Overlay CPU, memory, and I/O metrics. Look for the moment the failure rate crosses your baseline. That inflection point is your first real clue.

Step 3: Common Root Causes (and How to Confirm Them)

Intermittent failures aren’t magic. They fall into predictable categories. Here’s how to test each hypothesis quickly.

1. Resource Starvation

Thread pools, connection pools, file descriptors, memory. When a pool is exhausted, requests queue or fail. The failure is intermittent because it only happens under concurrency that exceeds capacity.

How to confirm: Graph pool utilization (active, idle, pending) alongside error rate. If errors spike exactly when pending queue depth rises, you’ve found it. Common culprits: database connection pools with too-low max connections, HTTP client pools without proper timeout/eviction, or unbounded caches filling the heap.

Fix: Increase pool size, add circuit breakers, or—better—find why requests are slow and fix the root cause. A slow downstream service can exhaust your upstream pool even if the pool size is “technically” correct.

2. Race Conditions and Deadlocks

Two goroutines, threads, or processes accessing shared state without proper synchronization. The bug only manifests when the scheduler interleaves them just right. These are notoriously hard to reproduce, but production leaves clues.

How to confirm: Look for log entries that are out of order, or operations that completed faster than physically possible (e.g., a “write confirmed” before the “write started”). Enable lock contention metrics in your database. For application-level races, use ThreadSanitizer or Go’s race detector in a canary deployment that mirrors production traffic—not in staging, which never has the same concurrency patterns.

3. Timeouts and Retry Storms

A downstream service slows down. Your service times out, retries, and the retries overload the downstream, causing more timeouts. This positive feedback loop is a retry storm. The failure is intermittent because it only triggers when latency crosses the timeout threshold, which might be rare.

How to confirm: Trace a single user request that failed. Check if multiple backend calls were made for the same logical operation. Look for duplicate database records or duplicate external API calls. If you see a spike in “409 Conflict” responses, that’s retries colliding.

Fix: Implement idempotency keys. Use exponential backoff with jitter. Set a maximum retry budget per request. And for the love of sanity, don’t retry on non-idempotent operations without deduplication.

4. Garbage Collection Pauses

In managed-runtime languages (Java, Go, .NET, Node.js), a GC pause can cause request timeouts. The pause is intermittent because it depends on heap state and allocation patterns.

How to confirm: Enable GC logging. Correlate GC pause times with latency spikes. In Go, use GODEBUG=gctrace=1 and scrape the logs. In Java, use -Xlog:gc*. If p99 latency jumps during a GC pause, you’ve found it.

Fix: Reduce allocation rate, tune GC parameters, or switch to a low-pause collector (ZGC, Shenandoah). Sometimes the fix is as simple as reusing objects instead of creating them per request.

Close-up of a circuit board with glowing traces

5. Network Blips and Partitioning

Packets get dropped. Switches fail over. Cloud provider networks have transient issues. Your system must handle these gracefully, but often it doesn’t.

How to confirm: Check TCP retransmit rates, network error counters (netstat -s), and cloud provider status history. Correlate with your error spikes. If you see SYN_SENT flood or connection refused errors, the network is suspect.

Fix: Implement proper connection pooling with health checks, fast failure detection (TCP keepalives, application-level heartbeats), and retry logic that respects idempotency. Use circuit breakers to isolate faulty dependencies.

6. Data Corruption or Inconsistent State

A database replica lags, a cache holds stale data, or a partial update leaves a record in an invalid state. The failure is intermittent because it depends on which node serves the request and the timing of replication.

How to confirm: Compare data returned from different replicas for the same query. Check cache hit rates and invalidation logs. Look for “phantom reads” or unexpected nulls in fields that should never be null.

Fix: Use strong consistency modes where correctness matters. Implement cache-aside with atomic invalidation. Add database constraints to prevent invalid state, not just application-level checks.

Step 4: Targeted Reproduction Techniques

Once you have a hypothesis, you need to reproduce the failure. But you can’t just “run it again” and hope. You need to amplify the suspected trigger.

Traffic mirroring: Capture production traffic and replay it against a canary instance with extra instrumentation. Tools like GoReplay or custom middleware can do this. The canary can have race detection, verbose logging, or experimental fixes.

Chaos engineering: If you suspect resource exhaustion, deliberately starve a test instance: limit CPU shares, cap memory, throttle I/O. If the error rate spikes, you’ve confirmed the mechanism. Use tools like tc (traffic control) to inject network latency and packet loss.

Deterministic simulation: For concurrency bugs, run the suspect code under a deterministic scheduler. This forces specific thread interleavings and makes the bug reproducible. It’s heavy lifting but sometimes the only way.

Step 5: The Fix Must Be Verifiable

Deploying a “maybe fix” and waiting to see if the error rate drops is amateur hour. You need a before/after comparison with statistical rigor. Define a clear metric (e.g., “p99 latency of /checkout endpoint”), collect a baseline over sufficient time, deploy the fix to a canary or a percentage of traffic, and compare distributions. Use a Kolmogorov-Smirnov test if you want to be formal, or just overlay histograms and eyeball the tail.

If the fix doesn’t change the metric, roll it back. Don’t leave dead code because “it might help somehow.” That’s how systems rot.

Step 6: Prevent Regressions

Once you’ve slain the dragon, make sure it stays dead.

  • Add a regression test: If you can’t reproduce the exact failure in CI, test the mechanism. For a connection pool exhaustion bug, write a test that saturates the pool and verifies graceful degradation.
  • Add a production monitor: Create an alert that fires when the conditions you identified (e.g., pool pending queue > threshold) are met, so you catch recurrence early.
  • Document the incident: Not a bloated post-mortem, but a concise record: symptoms, root cause, fix, detection method. Link it from the code. The next engineer will thank you.

Rows of server hard drives in a data center

FAQ

What’s the first thing I should check when an intermittent failure alert fires?

Check deployment history and config changes. A huge fraction of intermittent failures are caused by a recent change that didn’t get properly validated under production load. If something was deployed in the last hour, roll it back first, then investigate. Don’t debug a moving target.

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

You need to capture a full trace when it does happen. Set up conditional logging or tracing that triggers on the specific error signature and dumps the entire request context—headers, payload, downstream calls, timing. Treat each occurrence like a crime scene. Over time, patterns will emerge from the forensic data.

Is it ever acceptable to just add a retry and move on?

Only if you understand why the retry helps and you’ve confirmed the operation is idempotent. Blind retries mask symptoms and can cause retry storms. If you add a retry, also add a metric that counts retry attempts and alerts if the rate spikes. That way you’re buying time, not burying the problem.

What’s the most underrated tool for debugging intermittent failures?

Feature flags. They let you add debug logging, enable race detection, or test a fix on a small percentage of production traffic without a full deploy. If you don’t have a feature flag system, build one. It pays for itself the first time you isolate a heisenbug without redeploying at midnight.

Debugging intermittent failures is a discipline, not a lottery. Instrument ruthlessly, hypothesize from data, reproduce by amplifying triggers, and verify with metrics. Do that, and you’ll turn “flaky” into “fixed” faster than anyone expects.

Debugging Intermittent Failures in Production Systems: A No-Nonsense Guide

Intermittent failures in production are the worst kind of bug. They don’t happen often enough to trip alarms, but when they do, you’re left with confused users, corrupted data, and a knot in your stomach. You can’t reproduce them on demand. Logs look clean—until they don’t. And the pressure to fix them comes from everywhere: management, customers, and your own sleep-deprived brain. I’ve spent years chasing these ghosts across distributed systems, embedded devices, and high-frequency trading platforms. Here’s what actually works.

Why Intermittent Bugs Are Different

Most developers treat intermittent failures like regular bugs with a low reproduction rate. That’s a mistake. A deterministic bug has a clear cause and effect. An intermittent failure is a system state collision—two or more conditions that only cause failure when they align perfectly. Think of it as a Venn diagram where the overlap is your outage. Your job is to map that overlap without being able to see it directly.

Common triggers? Race conditions that only manifest under specific load patterns. Memory corruption from a code path nobody hits. A cosmic-ray-induced bit flip in non-ECC hardware. Garbage collection pauses that just barely exceed a timeout threshold. DNS resolution delays that cascade only when combined with a retry storm. The list is endless, but the approach to finding them is consistent.

Step 1: Stop Guessing and Start Measuring

Your first instinct is to read code and theorize. Resist it. Code review has its place later, but right now you need data. Intermittent failures leave fingerprints—you just need the right instrumentation to see them.

Instrument the Boundaries

Every production system has boundaries: API calls, database queries, message queue operations, file I/O. These are where timeouts, retries, and partial failures pile up. Add detailed logging around every boundary with:

  • Precise timestamps (millisecond resolution, minimum)
  • Request IDs that propagate across services
  • Latency measurements for each operation
  • Return codes, even for successes—you need to know when something almost failed

Don’t log “database query completed.” Log “DB query ‘getUserProfile’ returned 1 row in 234ms, connection pool size 8/20.” The pool size matters because intermittent failures often correlate with resource exhaustion that doesn’t quite hit the limit.

Capture State Snapshots on Failure

When an error does occur, grab everything. Thread dumps, heap histograms, connection pool states, in-flight request counts, CPU utilization, garbage collection metrics. You’ll only get a few failures to analyze before the pressure mounts, so make each one count. Tools like jstack for JVM systems, GDB for native code, or language-specific profilers can be triggered automatically on specific error conditions.

Server rack with blinking lights indicating system activity

Step 2: Build a Hypothesis from Patterns

Once you have rich failure data, look for correlations. This is detective work, not engineering. Plot failure timestamps against:

  • Deployment events (even unrelated ones—infrastructure changes have side effects)
  • Traffic patterns (failures often spike at 2 AM during backup windows, not at peak load)
  • Upstream service latency (a 50ms slowdown in an auth service can trigger cascading timeouts)
  • System clock adjustments (NTP slewing can break anything that compares timestamps)

I once tracked a “random” payment processing failure to a leap second insertion that caused a 1-second clock jump, which broke a monotonic time assumption in a locking library. The failure happened exactly once every 18 months. Without correlating timestamps to NTP events, we’d still be guessing.

Step 3: Force the Failure in a Controlled Environment

Reproducing intermittent bugs is about creating the conditions that make the failure probable, not about triggering the exact sequence. You need chaos engineering, but targeted.

If you suspect a race condition under load, don’t just run load tests—run them with network latency injection, CPU throttling, and clock skew. Tools like tc (traffic control) on Linux let you add 200ms of delay to specific ports. If the bug involves a database, run your test while a background process vacuums or reindexes. The goal is to widen the race window until the bug becomes reproducible on demand.

For memory corruption suspects, run under Valgrind or AddressSanitizer with a fuzzer hammering the inputs. Intermittent memory bugs often require a specific allocation pattern to overwrite the right bytes. A fuzzer combined with sanitizers can surface these in hours rather than months.

Close-up of network cables and server indicators

Step 4: Add Defensive Telemetry That Survives the Fix

Once you identify and patch the root cause, don’t rip out the instrumentation you added. Strip out the high-volume debug logs if they’re expensive, but keep the anomaly detectors. Add counters for the specific state combinations that caused the failure, and alert on them at low thresholds. The next intermittent bug will be different, but it will likely leave traces in the same boundary regions.

Implement distributed tracing if you haven’t already. OpenTelemetry or similar frameworks let you track a request across services and see exactly where time is spent. When a future intermittent failure occurs, you’ll have the timeline pre-built instead of reconstructing it from scattered logs.

Step 5: Write a Postmortem That Prevents Recurrence

A good postmortem doesn’t just document what happened—it identifies the systemic weakness that allowed the bug to reach production. Was the code review too focused on happy-path logic? Did the test suite lack timing variation? Was there no monitoring for the specific resource that became exhausted?

For intermittent failures, the postmortem should include a reproduction test that can be run in CI. If you can’t reproduce it deterministically, write a test that runs the suspect code path under randomized timing conditions for N iterations. A flaky test in CI is better than a flaky system in production—at least you’ll see it before users do.

Common Patterns and Their Fixes

Race Conditions in Asynchronous Code

You have two goroutines, threads, or async tasks that usually complete in order, but occasionally the second finishes first. The symptom is a null pointer, missing data, or incorrect state. The fix is explicit synchronization, but first verify the race exists. Add assertions that check ordering invariants. Run with race detectors enabled (Go’s -race flag, ThreadSanitizer for C++). If the race detector fires even once, you have your answer.

Resource Exhaustion Near Limits

Connection pools, thread pools, file descriptors, memory—all have limits. Intermittent failures happen when usage spikes just high enough to hit the limit, then immediately drops. Your monitoring shows 80% average utilization, but the 99th percentile touches 100%. Increase the limit or add backpressure. Better yet, graph the 99th percentile over time and alert when it exceeds 90% of capacity.

Timeouts and Retry Storms

A downstream service slows down slightly. Callers time out and retry. The retries add load, causing more timeouts. This positive feedback loop can turn a 50ms slowdown into a full outage. The fix: exponential backoff with jitter, circuit breakers, and request hedging (send the same request to multiple replicas and use the first response). But first, prove the storm exists by graphing retry rates against latency.

Developer analyzing system logs on multiple monitors

Garbage Collection Pauses

In managed runtimes, a GC pause can exceed your service’s SLA. The pause happens intermittently because it depends on allocation patterns and heap state. Enable GC logging with timestamps. Correlate GC pauses with latency spikes and errors. Tune the GC (e.g., switch to a low-pause collector like ZGC or Shenandoah) or reduce allocation pressure.

Clock Skew and Time Assumptions

Distributed systems often assume clocks are synchronized. They’re not. A node’s clock can drift seconds or even minutes before NTP corrects it. If your logic compares timestamps from different nodes, use logical clocks (Lamport timestamps, vector clocks) or tolerate skew explicitly. For absolute time, query a trusted time source rather than relying on local system time.

Tools That Earn Their Keep

Some tools are worth their weight in reduced debugging hours:

  • Wireshark/tcpdump: When you suspect network-level issues, capture packets at both ends. A TCP retransmission or reset that happens only under specific congestion conditions is invisible to application logs.
  • eBPF/BCC tools: Dynamic tracing without restarting processes. Trace kernel functions, syscalls, and user-space probes. Use tcplife to see short-lived connections that might be failing silently.
  • Chaos Monkey / Litmus: Not just for testing resilience—use them to reproduce intermittent failures by injecting the specific faults you suspect.
  • AlloyDB/PostgreSQL audit logging: For database-related intermittents, log every query with parameters and timing. A specific query pattern might only cause a deadlock when run concurrently with a maintenance job.

FAQ

How do I debug an intermittent failure that happens once a month?

You can’t wait for the next occurrence. Set up conditional logging that triggers on the precursors to the failure, not the failure itself. If the bug involves a specific user action, log detailed state for every session that performs that action. When the failure eventually occurs, you’ll have the data leading up to it. Also, run accelerated simulations: if the failure happens once per million requests, generate ten million requests in a test environment with fault injection.

What if the failure leaves no trace in logs?

Then your logging is insufficient. Add logging at every decision point in the affected code path. If you don’t know the affected code path, add logging at every external interaction (network calls, disk writes, lock acquisitions). Use binary logging or ring buffers if volume is a concern. For crashes without stack traces, configure core dumps and ensure they’re captured even in containerized environments (set core_pattern in the host’s sysctl).

How do I convince management to give me time to fix an intermittent bug properly?

Quantify the impact. Calculate the error rate multiplied by affected users, then estimate revenue loss or support cost. Compare the cost of proper debugging (including instrumentation and chaos testing) against the cost of recurring incidents over a year. If the bug causes a 0.1% failure rate but affects a payment system processing $10M/day, that’s $10K/day in direct losses. Present the numbers, not the technical details. Management understands money.

Can I just restart the service and hope it goes away?

Restarting masks the symptom temporarily. If the bug is caused by slow resource accumulation (memory leak, connection leak, log file filling disk), restarting resets the counter and buys you time. But the bug will return, and the interval will shrink as load grows. Use the bought time to add instrumentation so you can catch it next cycle. Document the restart frequency and set an alert when it exceeds a threshold—that turns the restart itself into a signal.

Final Word

Intermittent failures are not magic. They are deterministic outcomes of specific, rare state combinations. Your job is to make those states visible and then make them impossible. Every hour spent guessing is an hour the bug stays in production. Instrument first, hypothesize second, reproduce third, fix fourth. In that order. Always.

The Cold Reality of Debugging Intermittent Production Failures

Intermittent failures in production don’t give a damn about your deadlines. They don’t care about your test coverage, your staging environment, or that shiny observability stack you spent months wiring up. They show up when they feel like it, vanish the moment you try to look, and leave you with nothing but a sour stomach and a ticketing queue full of user rage. If you’re reading this, you’ve probably been burned. You’ve stared at logs that offer no clues, replayed traffic that behaved perfectly, and questioned your life choices while a system you built gaslights you at 3 a.m. I’ve been there. This isn’t a guide for beginners who think more unit tests fix everything. This is a blunt, technical walkthrough on actually hunting down and killing these gremlins, built from scars earned on production systems chewing through millions of requests a day.

Let’s get one thing straight: intermittent failures aren’t random. They’re deterministic in a system you don’t fully understand yet. The state space of a modern distributed application is massive—thread scheduling, network timing, garbage collection pauses, cache eviction, disk I/O jitter, and a dozen other things you forgot about. The failure only triggers when a precise, unlikely cocktail of these factors lines up. Your job is to shrink that unknown space until the pattern stops hiding. This takes a different mindset from normal debugging. You’re not hunting for a broken line of code. You’re hunting for a broken assumption about how the system behaves under real, filthy conditions.

Stop Guessing and Start Measuring

Most engineers, when they hit a transient bug, dive straight into code review or try to reproduce it locally. That’s a waste of time. You cannot reproduce a production-only race condition on your MacBook. First step: define the problem with hard numbers. What exactly is failing? A specific API endpoint? A background job? A database query? Narrow it down by error rate, latency percentile, and affected user segments. If your monitoring won’t let you slice by those dimensions, fix that first. You can’t debug what you can’t see.

Instrument the failing path with targeted metrics. Not generic request counts—specific counters for each branch, each external call, each retry attempt. Add histograms for operation durations, especially around suspected race windows. Log the state that matters when a failure occurs: thread IDs, transaction IDs, queue depths, connection pool status. Standard logging hides the signal because it’s tuned for normal ops. You need failure-specific logging that grabs the system’s state at the exact moment of the anomaly, before it evaporates.

Server rack with glowing LED indicators showing network activity
Production hardware doesn’t care about your local environment. Measure what’s actually happening.

Hypothesis-Driven Investigation

You can’t just “look at the data” and expect the bug to wave at you. You need a structured approach. Write down a list of specific, falsifiable hypotheses. For example: “The failure occurs only when a connection is borrowed from the pool while another thread is closing an expired connection.” Or: “The timeout happens because the garbage collector pauses longer than the request deadline during a full heap compaction.” Each hypothesis should predict a measurable signature: a correlation between GC pause times and error spikes, a specific ordering of log entries, a memory pressure metric breaching a threshold.

Then design experiments to test the cheapest, most likely hypotheses first. That might mean a canary deployment with extra diagnostics, deliberately inducing load patterns on a staging cluster that mimic production traffic shape, or running a chaos experiment that triggers the suspected condition. The key: don’t change too many variables at once. If you tweak three thread pool sizes, enable verbose GC logging, and upgrade a library all at the same time, you’ll never know what fixed it—or worse, you’ll paper over the real cause and it’ll come back months later under heavier load.

Common Culprits That Hide in Plain Sight

Over the years, I’ve seen the same classes of intermittent failures repeat across different stacks and architectures. Check these before you chase exotic theories.

Connection pool exhaustion. Your pool size looks fine under steady load, but a brief burst of slow responses from a downstream service causes threads to block waiting for connections. By the time the downstream recovers, your request has timed out, but the connection is still checked out. The pool drains, subsequent requests fail fast, then everything recovers. The logs show a timeout, then a bunch of connection errors, then silence. The fix isn’t always a bigger pool—it’s often circuit breakers, correctly propagated timeouts, and understanding your pool’s max wait behavior.

Garbage collection stalls. In managed runtimes like the JVM or CLR, a full GC pause can stop all application threads for seconds. If your health checks or request deadlines are shorter than that pause, you get transient failures with no application-level error. GC logs will show the pause, but most teams never look until they’re desperate. Modern collectors reduce pause times but don’t eliminate them, especially if you’re allocating large objects or have a huge heap. Learn your runtime’s GC flags and monitor pause times as a first-class metric.

Cache stampedes with TTL expiry. A hot cache key expires, and a hundred concurrent requests all try to regenerate it simultaneously. Your database melts, some requests time out, and the cache gets populated with a partial or error value. The failure looks random because it hinges on the exact alignment of TTL windows and request arrival times. The fix is usually a mix of probabilistic early recomputation, locking on cache miss, or external refresh processes that never let the cache go cold.

DNS and service discovery lag. A downstream host gets yanked from the load balancer pool, but your process still holds a stale cached IP. Connections to that host fail until the cache expires, which might be minutes later. If your retry logic is naive, it retries the same dead host. The failure rate matches the traffic proportion hitting that cached entry. This is especially nasty in containerized environments where IPs churn fast.

Network cables and patch panel connections in a data center
Stale DNS caches and routing changes cause failures that look random but are perfectly deterministic.

Production-Safe Probing Techniques

You can’t always reproduce the issue in staging, and you can’t trash production with reckless experiments. So you need techniques that gather evidence without piling on risk. Feature flags are your friend. Wrap suspicious code paths with a flag that lets you crank up logging verbosity or enable extra state captures for a small percentage of traffic. That gives you detailed failure data from real users without drowning your log storage.

Dark traffic replay is another powerful tool. Capture a sample of production requests—especially the ones that failed—and replay them against a canary instance running with extra instrumentation. You can tweak the replay to vary timings, inject delays, or shuffle ordering to trigger race conditions. The instance doesn’t serve real users, so crashes and slowdowns are acceptable. The hard part is sanitizing sensitive data and ensuring your replay doesn’t trigger side effects like duplicate payments. It’s engineering work, but it pays off when the bug is truly elusive.

Kernel and network-level tracing can surface problems application logs miss. Tools like tcpdump, strace, or eBPF-based probes let you see system calls, packet retransmissions, and scheduling delays. An intermittent “slow request” might be a 200ms TCP retransmission caused by a saturated NIC, not a slow database query. Your application sees a long wait, logs a timeout, and points the finger at the wrong component. Correlating application spans with kernel events closes this observability gap.

Correlation Traps and Timing Bugs

The most dangerous intermittent failures are the ones that correlate with something else in a deceptive way. You see a spike in errors every time a deployment happens, so you blame the deployment. But the deployment restarts processes, which resets connection pools, which briefly spikes load on backends, which triggers the real bug. The deployment isn’t the cause; it’s a catalyst. If you just roll back the deployment and the errors stop, you’ll think you fixed it. You didn’t. It will return during the next traffic spike or failover event.

Timing bugs are another special hell. A thread checks a condition, then a context switch happens, then another thread changes the state, then the first thread acts on the stale check. Classic TOCTOU (time-of-check to time-of-use) race. It might only happen under specific CPU load patterns that affect thread scheduling quanta. Reproducing it takes stress testing with controlled scheduling delays, often using tools that inject sleep() calls at strategic points. The fix is usually a proper locking strategy or an atomic compare-and-swap operation, but first you have to find the window.

Close-up of a circuit board with intricate electronic pathways
Race conditions live in the microscopic gaps between instructions. Don’t trust code that looks correct in isolation.

Postmortem Rigor Without the Theater

Once you’ve found the bug, don’t just patch it and move on. A real fix addresses the systemic weakness that let the bug exist undetected and made the failure so hard to diagnose. Your postmortem should answer: Why didn’t our tests catch this? Why didn’t our monitoring alert us sooner? Why didn’t our runbooks help the on-call engineer? These questions sting because they expose gaps in your engineering practices, not just a single coding mistake.

For intermittent failures specifically, ask whether your system’s design made the failure mode inevitable. Did you assume a network call would always complete within a timeout that’s too close to the p99 latency? Did you lean on a cache with no fallback or graceful degradation? Did you use a library with known thread-safety issues because it was convenient? The fix might demand architectural changes—adding backpressure, switching to an event-driven model, or tearing out shared mutable state. If that’s what’s needed, say it plainly in the postmortem, even if it means delaying feature work.

Update your tests. Integration tests that run with realistic concurrency and network delays can catch many race conditions. Use property-based testing to explore edge cases around timeouts and retries. Write chaos tests that deliberately kill processes, partition networks, and skew clocks. These tests are expensive to maintain and slow to run, but they’re the only defense against the class of bugs that only appear when the universe lines up against you.

FAQ

Why can’t I reproduce the failure in my local environment?

Your local environment lacks the concurrency, data volume, network variability, and resource contention of production. A single-threaded debug run won’t trigger race conditions. A local database with sub-millisecond latency won’t expose timeout edge cases. You need production-like load profiles and state to trigger the failure. Accept that local reproduction is a bonus, not a requirement, and shift your debugging to production-safe observation techniques.

What’s the fastest way to prove a race condition exists?

Stress-test the suspicious code path with high concurrency and deliberately injected timing jitter. If you can make the failure rate climb by adding small random delays near the suspected race window, you’ve found your culprit. Tools like tc for network delay, stress for CPU contention, or custom aspect-oriented instrumentation help. Once you can modulate the failure rate, you have a means to isolate the exact lines of code involved.

How do I convince management to invest in fixing an intermittent bug that “only happens sometimes”?

Translate “sometimes” into business impact. Calculate the error rate over a month, multiply by the value of affected transactions, and add the engineering time spent firefighting. An intermittent bug causing a 0.1% failure rate on a high-value checkout flow can cost millions each year. Pitch the fix not as a technical improvement but as risk reduction: the same underlying condition could trigger a cascading failure under higher load. Management gets risk and money. Use their language.

Debugging Intermittent Failures: A Field Manual for Engineers Who Hate Guessing

The Phantom in the Machine

Intermittent failures are the worst kind of bug. They don’t show up on demand. They don’t leave a tidy stack trace. They mock your dashboards and laugh at your unit tests. I’ve lost count of the nights I’ve stared at logs that showed absolutely nothing wrong—while some service quietly bled out at 3 a.m. This isn’t a whitepaper. It’s a field manual for engineers who are done guessing.

You know the drill. A service hums along for hours, then spews 500s for two minutes. No deploy. Traffic’s flat. The database isn’t melting. But somewhere, a thread panicked, a connection pool ran dry, or a timeout triggered a retry avalanche. The real cause is buried under async calls, stale caches, and load balancers that hide the dying node. You need to treat the system like a crime scene, not a whiteboard.

Close-up of a server rack with blinking lights, representing the opaque nature of production infrastructure

Why Your Logs Are Lying to You

Most teams start by grepping for errors. That’s a dead end. Intermittent failures rarely leave a single smoking gun. You get a scatterplot of symptoms: a latency bump on one endpoint, a burst of connection resets, a thread pool exhaustion warning that clears itself. The real culprit is often a resource leak that only triggers under specific concurrency, or a race condition that depends on the exact sequence of network responses. Standard logging is too blunt. You need to instrument the boundaries.

Start by adding structured logs at every I/O edge: database calls, cache lookups, HTTP requests to downstream services, message queue pushes. Capture the duration, the status, and a correlation ID that travels across threads. Don’t just log errors—log the happy path too. When the failure hits, you can compare the timing of successful operations against the ones that tanked. Look for patterns: a cache miss that kicks off a thundering herd, a database query that suddenly takes 2 seconds instead of 20ms, a downstream service that returns 200 OK with an empty body. The failure is almost never where the exception gets thrown.

Correlation IDs: The Spine of Your Investigation

If you don’t have correlation IDs propagating through every service, stop reading and go implement that. A single request should carry an ID that gets handed to every downstream call, every log line, every span. Without it, you’re assembling a jigsaw puzzle in the dark. With it, you can trace one failed request across a dozen microservices and pinpoint exactly where the delay or error started. Use a standard header like X-Request-ID and enforce it at the API gateway. Make sure your HTTP clients, database drivers, and message producers all forward it. This is not a nice-to-have.

Reproducing the Unreproducible

You can’t fix what you can’t trigger. But reproducing an intermittent failure in production is risky, and staging often won’t cut it. The answer is to isolate the component and stress it under controlled conditions. If the failure correlates with high connection counts, write a script that opens hundreds of connections and measures response times. If it happens during deploys, simulate rolling restarts while traffic flows. Use traffic shadowing to replay production requests against a canary instance with extra diagnostics turned on.

Chaos engineering isn’t just for Netflix. Start small: inject latency into a dependency, drop a percentage of packets, or kill a pod and watch the retry logic squirm. The goal is to trigger the failure mode in a way that leaves evidence. Once you can reproduce it, you can bisect the codebase, add assertions, and narrow the cause. Until then, you’re reading tea leaves.

Network cables in a data center, symbolizing the complex connections where failures hide

Using Traffic Shadowing Safely

Traffic shadowing duplicates live requests to a test instance that doesn’t touch real users. Tools like GoReplay or Envoy’s request mirroring can do this. The shadow instance runs with verbose logging, debug symbols, and sanitizers enabled. It can crash without consequences. When it does, you get a core dump and a full trace. This is how you catch the memory corruption that only happens on request #10,347 with a specific payload. Just make sure the shadow instance never writes to real databases or publishes to real queues. One misconfigured shadow can cause a very real outage.

Diagnostic Tooling That Actually Works

Stop relying on printf debugging. Production systems need live introspection. Here’s what belongs in your toolkit:

  • Thread dumps: For JVM or CLR apps, capture thread dumps during the incident. Look for threads stuck in BLOCKED state, waiting on the same monitor. That’s your lock contention. Look for threads in RUNNABLE that never finish—that’s a CPU spin or infinite loop.
  • Heap profiles: Memory leaks cause intermittent failures when GC pauses spike. Use heap dumps or continuous profiling (async-profiler, dotMemory) to see what’s eating memory over time. A slow leak can take days to trigger a failure.
  • Network packet captures: When services blame each other, capture packets with tcpdump or Wireshark. Look for TCP retransmissions, RST packets, or half-open connections. A misconfigured keep-alive or a load balancer dropping idle connections can cause intermittent timeouts that look like application bugs.
  • Distributed tracing: Jaeger, Zipkin, or Honeycomb. Traces show you the exact waterfall of a request. Find the trace where the failure happened and look for the span with the anomalous duration or error tag. That’s your culprit service.

Profiling in Production Without Killing Performance

Many engineers fear profilers because of overhead. Modern sampling profilers like async-profiler for JVM or perf for Linux add less than 1% overhead. Run them continuously and dump the output when an SLO breaches. You’ll see exactly which functions were on-CPU during the slowdown. I once found a regex that compiled on every request because the cache key was using the wrong object identity. A 30-second profile during the incident showed it immediately. Logs were silent.

Common Culprits and Their Signatures

Over years of debugging these nightmares, I’ve catalogued repeat offenders. Check these first:

  • Connection pool exhaustion: Symptoms: requests hang, then timeout. Metrics show pool size hitting max, with wait times spiking. Cause: leaking connections (not closed on exceptions), slow database queries holding connections, or missing connection timeout settings.
  • Thread pool saturation: Symptoms: increased latency, rejected tasks, RejectedExecutionException in logs. Cause: blocking calls inside non-blocking thread pools, unbounded queues masking backpressure, or downstream latency causing threads to pile up.
  • Garbage collection storms: Symptoms: periodic latency spikes, STW pauses visible in application logs as gaps. Cause: memory leaks, large object allocations, or misconfigured GC settings for the workload.
  • Race conditions under load: Symptoms: data corruption, duplicate processing, or state inconsistencies that vanish under low traffic. Cause: unsynchronized access to shared mutable state, missing happens-before relationships, or optimistic locking failures that aren’t retried.
  • Cache stampedes: Symptoms: sudden spike in database load when a popular cache key expires. Cause: many threads simultaneously detect a cache miss and all fetch the same data, overwhelming the backend.

Close-up of a glowing fiber optic cable, representing the high-speed data paths where intermittent issues occur

Building a Hypothesis-Driven Investigation

Don’t just stare at dashboards. Form a hypothesis and try to disprove it. Start with the symptom: “Requests to /checkout timeout for 2% of users between 14:00 and 14:05 UTC.” List every component in the request path: CDN, load balancer, API gateway, auth service, checkout service, payment service, database, cache. For each, ask: “What would cause this component to intermittently fail?” Then check the evidence.

For the database, check slow query logs for that time window. For the cache, check eviction rates and memory usage. For the payment service, check its upstream latency and error rates. Cross-reference with deployment logs, cron job schedules, and traffic patterns. Often the trigger is a batch job that runs every hour and saturates the database, or a cache flush that causes a thundering herd. Eliminate components one by one until the evidence points to a single cause.

Time-Correlating Events Across Systems

Clocks drift. Don’t trust timestamps from different machines unless they’re NTP-synced and you’ve accounted for skew. Use a centralized logging system that stamps events on ingestion with a monotonic clock. When comparing logs from two services, align them by the correlation ID, not the timestamp. I’ve wasted hours chasing phantom delays that were just clock skew between app servers and database nodes.

Fixing Without Breaking

Once you identify the root cause, resist the urge to push a fix immediately. Intermittent failures often mask deeper architectural flaws. A quick patch—increasing a timeout, enlarging a pool—might hide the symptom but leave the underlying race condition or resource leak. That leak will just take longer to blow up, and when it does, it’ll be worse.

Instead, apply a temporary mitigation to stop the bleeding, then design a proper fix. If the issue is connection pool exhaustion, add circuit breakers and fail-fast logic so the system degrades gracefully instead of hanging. If it’s a race condition, fix the synchronization but also add assertions and monitoring to detect similar patterns elsewhere. Every intermittent failure is a gift: it exposes a boundary condition your tests missed. Capture that condition as a regression test, even if you have to simulate it with fault injection.

Circuit Breakers and Bulkheads

These patterns from resilience engineering are not optional for production systems. A circuit breaker stops calling a failing dependency after a threshold of errors, giving it time to recover and preventing cascading failures. A bulkhead isolates components so that a failure in one doesn’t exhaust resources for others (e.g., separate thread pools for different downstream calls). Implement them with libraries like Resilience4j or Polly, but understand the semantics. A circuit breaker that trips too eagerly can cause its own outages. Tune thresholds based on real traffic patterns, not guesses.

FAQ

Why do intermittent failures often happen at specific times?

Because they’re triggered by periodic events: cron jobs, cache expirations, log rotations, or traffic patterns that hit a threshold. A database backup at 2 AM can cause I/O contention that slows queries just enough to trigger timeouts. A cache TTL of exactly one hour can cause a stampede every 60 minutes. Check your system’s scheduled tasks and align them with failure timestamps.

How do I debug a failure that leaves no trace in logs?

If there’s no error log, the failure is likely at a lower layer: network, OS, or hardware. Check kernel logs for OOM kills, check network interface statistics for packet drops, and monitor system metrics like CPU steal time (if virtualized) or disk I/O wait. Use eBPF tools to trace syscalls without overhead. Sometimes the application never sees the failure because the kernel or hypervisor silently drops connections.

What’s the fastest way to find the root cause during an active incident?

Don’t try to find the root cause during the incident. First, restore service by rolling back recent changes, scaling up resources, or failing over to a redundant system. Then, once users are happy, start the investigation with the evidence you preserved: thread dumps, heap dumps, trace samples, and snapshots of key metrics from the moment of failure. Debugging under pressure leads to bad fixes.

Prevention: Design for Debuggability

The best time to debug an intermittent failure is before it happens. Build your system with debuggability as a first-class requirement. That means:

  • Structured logging with consistent fields: Every log line should include a timestamp, severity, service name, correlation ID, and a message that describes what happened, not how you feel about it. Use JSON so you can query fields without regex gymnastics.
  • Live metrics with high cardinality: Don’t pre-aggregate everything. Keep per-endpoint latency percentiles, per-client error rates, and per-node resource usage. When a failure affects only one availability zone or one customer, aggregated metrics hide it.
  • Distributed tracing by default: Sample a fraction of requests (start with 1%, adjust based on cost) and always sample errors. A trace of a failed request is worth a thousand log lines.
  • Fault injection in CI/CD: Run chaos experiments in your staging environment on every build. If you can’t break it intentionally, you won’t find the bugs before production does.

Postmortems That Prevent Recurrence

When you finally fix the bug, don’t just close the ticket. Write a postmortem that captures the timeline, the impact, the root cause, and—most importantly—the detection and prevention improvements. How long did it take to notice the failure? Could monitoring have caught it sooner? Did the runbook have a diagnostic step for this symptom? Update the runbook. Add a metric. Create a playbook for the next engineer who sees a similar pattern. The goal is to make this class of failure impossible or instantly diagnosable next time.

Intermittent failures are a tax on sloppy engineering. Pay the tax once by fixing the root cause and improving the system’s observability. Or pay it repeatedly with 3 AM calls and unhappy users. The choice is yours.

Debugging Intermittent Failures in Production: A No-Nonsense Guide

Intermittent failures in production are the worst kind of bug. They don’t happen every time. They don’t leave a clean stack trace. They mock your attempts to reproduce them in staging. And when they strike, they usually do it at 3 a.m. on a Saturday. I’m Felix Okonkwo, and I’ve spent more nights than I care to count staring at dashboards that flicker red then green for no obvious reason. This article is a direct, technical walkthrough of how I approach these problems—no fluff, no theory, just what works when the heat is on.

Why Intermittent Failures Are Different

A deterministic bug is a gift. You can reproduce it, isolate it, and fix it. An intermittent failure is a statistical event. It depends on timing, load, data patterns, or environmental conditions that align only occasionally. The failure rate might be 0.1% of requests, but when you serve a million requests a day, that’s a thousand angry users. The core challenge is that your normal debugging toolkit—breakpoints, step-through, local repro—is useless. You have to think in terms of signals, not symptoms.

Most engineers reach for logs first. That’s correct, but insufficient. Logs tell you what happened at a single point in time. Intermittent failures are often about what didn’t happen: a timeout that didn’t fire, a lock that wasn’t released, a cache entry that expired a millisecond too early. You need to reconstruct the system’s state across multiple services and time windows. This requires a different mindset.

Server rack with blinking lights indicating intermittent activity

Step 1: Define the Failure Signature Precisely

Before you grep a single log line, write down exactly what “intermittent” means in this case. Is it a 500 error on a specific endpoint? A spike in p99 latency? A dropped message from a queue? Quantify it: error rate over time, affected user IDs, geographic distribution, time-of-day patterns. If you can’t describe the failure in a single sentence with numbers, you’re not ready to debug.

For example: “Between 14:00 and 14:15 UTC, the /checkout API returned HTTP 502 for 0.3% of requests, exclusively for users in the us-east-1 region, and only when the request payload exceeded 8KB.” That’s a signature. Now you have a filter, not a haystack.

Use your monitoring tools to slice the data. Pull error rates by endpoint, by status code, by instance, by availability zone. Correlate with deployment timestamps, config changes, and traffic patterns. Often the “intermittent” label is just a lack of granularity. A failure that looks random at the service level might be perfectly consistent for a specific pod or a specific database shard.

Step 2: Instrument the Boundaries, Not the Internals

When a request fails intermittently, the root cause is rarely inside your application logic. It’s at the boundaries: network calls, database queries, message broker interactions, external API invocations. These are the points where timing matters most. Add detailed logging or metrics around every boundary call. Capture not just success/failure, but latency, payload size, connection reuse, and retry attempts.

I keep a standard checklist for boundary instrumentation:

  • Outbound HTTP: Log target host, request duration, status code, and whether a connection was reused or newly established. Intermittent 502s often trace back to a load balancer that recycles idle connections while your app’s connection pool still thinks they’re alive.
  • Database queries: Log the query plan hash, execution time, rows returned, and transaction isolation level. A query that runs in 2ms 99% of the time but spikes to 2s when a particular index falls out of the buffer cache is a classic intermittent culprit.
  • Message brokers: Log publish confirmations, consumer lag, and redelivery counts. A message that’s silently redelivered after a consumer timeout can cause duplicate processing that fails on unique constraints.
  • Cache operations: Log hit/miss, TTL at time of access, and serialization errors. A near-expiry cache entry that returns stale data under high concurrency is a race condition waiting to happen.

Close-up of network cables and switch ports

Step 3: Trace a Single Failed Request End-to-End

If you have distributed tracing (Jaeger, Zipkin, or a vendor solution), this is where it earns its keep. Find a trace ID for one failed request. Don’t look at aggregates—look at the exact span timeline. Compare it to a successful request with the same parameters. The difference is often a single span that took 50ms longer, or a span that’s missing entirely because a service dropped the trace context.

Without tracing, you can build a poor-man’s trace using correlation IDs. Generate a unique ID at the edge, pass it through every service via headers or message metadata, and log it at every boundary call. Then grep that ID across all your log streams. Reconstruct the timeline manually. It’s tedious, but it works.

Pay attention to the order of operations. In a successful request, the database query might complete before the cache update. In the failed request, the cache update might finish first due to a thread scheduling hiccup, causing a subsequent read to see stale data. These ordering anomalies are invisible in aggregate metrics.

Step 4: Stress the System Asymmetrically

You can’t reproduce the failure in staging because staging doesn’t have production’s traffic shape. Production traffic has bursts, slow clients, retransmits, and weird user agents that send double requests. To surface the bug, you need to stress the system in ways that mimic these patterns.

Here are techniques I use:

  • Connection pool exhaustion: Artificially limit the pool size to 1 and blast concurrent requests. This exposes connection leaks and deadlock-prone code paths.
  • Slow client simulation: Introduce a proxy that delays reading response bytes. This triggers write timeouts and reveals incomplete response handling.
  • Clock skew injection: If your system uses timestamps for ordering (e.g., last-write-wins), skew the clocks on different nodes by a few seconds. This surfaces race conditions that depend on NTP sync.
  • Data pattern replay: Capture a sample of production payloads that correlate with failures, then replay them at high volume. A specific JSON field that’s null 0.1% of the time can break deserialization if your code doesn’t handle it.

Run these tests in a pre-production environment that mirrors production as closely as possible—same instance types, same database version, same kernel parameters. The goal is not to replicate the exact failure, but to create conditions where the underlying defect becomes deterministic.

Step 5: Inspect the Garbage Collector and Runtime

Intermittent failures in managed-runtime languages (Java, .NET, Go, Node.js) often correlate with garbage collection pauses or runtime scheduler delays. A GC pause that lasts 200ms can cause a health check timeout, which triggers a pod restart, which drops in-flight requests. The failure looks like a network blip, but the root cause is memory pressure.

Enable GC logging with timestamps. Overlay GC pause times on your request latency graph. If latency spikes align with GC events, you have a memory problem. Common fixes: reduce allocation rate, tune heap size, or switch to a low-pause collector. For Go, look at goroutine scheduling delays using the execution tracer. A goroutine that’s starved for CPU because of a tight loop in another goroutine can delay request processing just enough to hit a deadline.

Code on a monitor with debugging tools open

Step 6: Audit Your Retry and Timeout Logic

Ironically, the mechanisms designed to handle transient failures often cause intermittent failures. Retry storms, where a failed request is retried by multiple layers (client library, service mesh, application code), can amplify a small blip into a full outage. Timeouts that are too short turn normal latency variance into errors. Timeouts that are too long cause resource exhaustion as threads block waiting for dead backends.

Map every retry and timeout in your request path. For each one, ask:

  • Is the timeout value based on measured p99 latency plus headroom, or is it a guess?
  • Does the retry use exponential backoff with jitter, or does it hammer the backend at a fixed interval?
  • Is there a retry budget (e.g., max 3 attempts per request) to prevent infinite loops?
  • Are retries idempotent? A retried POST that creates a duplicate resource is a data corruption bug masquerading as a transient error.

I once spent two weeks chasing an intermittent “duplicate key” error in a database. The culprit was a client library that retried inserts on connection timeouts without checking if the original insert had succeeded. The fix was adding an idempotency key, not tweaking the database.

Step 7: Check the Infrastructure Layer

Application code gets the blame, but the infrastructure is often the silent accomplice. Load balancer health checks that flap between healthy and unhealthy cause requests to be routed to dying instances. Kubernetes readiness probes that don’t account for warm-up time kill pods before they’re ready to serve. Network overlays drop packets when MTU settings mismatch.

Correlate application errors with infrastructure events: pod restarts, node rebalancing, security group changes, TLS certificate rotations. A 0.1% error rate that spikes every 90 days might align with certificate renewal, where a few clients reject the new cert due to an outdated trust store. These are not code bugs, but they manifest as code bugs.

If you’re on a cloud provider, pull the infrastructure event logs for the affected time window. Look for maintenance notifications, host migrations, or network path changes. A brief network partition that isolates a few nodes can cause leader election chaos in a distributed system, resulting in split-brain writes that fail silently.

Step 8: Use Production Traffic for Testing (Carefully)

When all else fails, you need to experiment in production. This is not reckless if you do it with controls. The key is to minimize blast radius while maximizing signal.

Techniques I’ve used successfully:

  • Traffic shadowing: Mirror a percentage of production traffic to a debug instance with extra logging. The shadow instance doesn’t affect real users, but it sees the same input patterns.
  • Canary with debug mode: Deploy a single instance with verbose logging or a profiler attached, and route a small fraction of traffic to it. Monitor error rates closely and be ready to pull it out if performance degrades.
  • Feature flag for detailed tracing: For authenticated users, enable a flag that logs full request/response bodies for a random sample of sessions. This captures the exact payload that triggers the bug, but requires strict data privacy controls.

These techniques are invasive. They add overhead and risk exposing sensitive data. Get sign-off from your security team, set strict sampling rates, and have a kill switch ready. But when the bug only appears under real user behavior—a specific sequence of clicks, a particular mobile carrier’s header injection—this is the only way to catch it.

Step 9: Write a Postmortem That Prevents Recurrence

Finding the bug is half the battle. The other half is making sure it doesn’t come back in a different form. A good postmortem for an intermittent failure doesn’t just document the fix—it documents the detection gap, the diagnostic path, and the systemic changes that will surface similar issues faster next time.

Your postmortem should answer:

  • What monitoring alert would have caught this before users noticed? Add it.
  • What log or metric was missing that would have shortened the investigation? Instrument it.
  • What assumption about timing, ordering, or idempotency was violated? Codify it as a design review checklist item.
  • What test—chaos experiment, load profile, data fuzzer—would have caught this in staging? Automate it.

Intermittent failures are a symptom of systems that have grown beyond their original assumptions. Every one you fix is an opportunity to harden the system against the next one. That’s the job. Don’t just patch the code—patch the process.

FAQ: Intermittent Failure Debugging

What’s the first thing I should check when an intermittent error appears?

Check for recent changes: deployments, config updates, infrastructure modifications. Even if the change seems unrelated, it often shifts timing or resource usage enough to surface a latent bug. If you have a change log, correlate the error’s first appearance with the change timeline. If you don’t have a change log, start one now—it’s the cheapest diagnostic tool you’ll ever build.

How do I debug an intermittent failure that leaves no logs?

If the failure is silent—requests simply disappear or return empty responses—look at the network layer. Capture packets on the affected hosts using tcpdump or a service mesh’s tap feature. Compare a successful and failed request at the packet level. Look for RST packets, TCP retransmissions, or TLS handshake failures. Silent failures are often network drops that occur before your application code runs.

Why do intermittent failures often get worse over time?

Because the underlying condition—a slow memory leak, a growing database table, an accumulating log file—worsens gradually until it crosses a threshold. What was a 0.01% failure rate becomes 0.1%, then 1%. Plot the error rate over a long time window (weeks, not hours). If it’s trending upward, you’re dealing with a resource exhaustion problem, not a random race condition. Look at memory, disk, file descriptors, and connection counts on the affected instances.

Can intermittent failures be caused by client-side behavior?

Absolutely. Clients that retry aggressively, send malformed headers, or close connections mid-request can trigger bugs in server-side code that assumes well-behaved clients. If the failure correlates with specific user agents, IP ranges, or geographic regions, instrument your edge layer to capture raw request data for those segments. You might find a mobile app that sends double requests due to a bug in its networking library, causing race conditions on your end.

Debugging intermittent failures is a discipline, not a talent. It’s about systematic signal collection, boundary analysis, and a refusal to accept “cannot reproduce” as an answer. The systems we build are deterministic at the hardware level; every failure has a cause. Your job is to find the right lens to see it.

Debugging Intermittent Production Failures Without the Guesswork

The Reality of Intermittent Failures

Production systems break in ways that would embarrass a deterministic debugger. An intermittent failure is the kind of defect that shows up only under a weird alignment of conditions. I’ve woken up to timeouts that fire at 3:14 AM every third Tuesday, data corruption that only appears when the garbage collector decides to run during a traffic spike, and deadlocks that demand seven simultaneous requests with the exact right interleaving. If you’ve ever been paged for something that fixed itself before your laptop finished booting, you already know that isn’t a resolution. That’s just the universe giving you a pass.

The real headache with intermittent failures is that they mock everything you learned about stepping through code. You can’t attach a debugger to an operation that fails once every ten thousand runs. You need a process that treats production as the only honest test environment. This article walks through a methodical sequence for identifying, reproducing, and crushing those failures. No wishful thinking. No ritual restarts. No hoping the problem just gets bored and leaves.

Server rack with blinking lights indicating complex infrastructure

Step One: Define the Failure Signature

Before you touch any code, pin down exactly what the system does when it fails. This isn’t the same as defining the bug. The failure signature is the observable symptom: a 503 error, a duplicated database record, a dropped message, a corrupted response payload. Write it down in one sentence. Something like, “The payment service returns HTTP 500 with a connection reset error after processing roughly 12,000 requests.” That single line forces clarity. Vague complaints like “it gets slow sometimes” belong in a user forum, not an incident channel.

Next, grab every scrap of metadata you can. Timestamps, affected endpoints, instance IDs, request IDs, user agents. If your logging is sparse, stop what you’re doing and fix that. Intermittent failures demand structured logs with trace context propagation. No correlation ID that follows a request across service boundaries? You’re blind. I’ve watched teams waste weeks because they logged errors without attaching the originating request identifier. Don’t be that team. It’s depressing.

Instrumenting for Intermittent Failures

Add targeted instrumentation around the suspected area. Skip the print-statement confetti. Use counters, histograms, and log sampling instead. Say a database call flakes out intermittently. Log the query duration, connection pool state, and transaction isolation level at the moment of failure. If you’re on a metrics library, expose a counter for that specific error condition and graph it against request rate, memory pressure, and file descriptor count. You’re hunting for a pattern, not a confession.

One tactic I lean on is logging a detailed trace only when an operation blows past a latency threshold. Imagine the service normally responds in 50ms. Set a conditional log that triggers at 500ms. That captures the slow path without drowning your log aggregator in noise. The point is to make the failure reproducible by recording enough state to rebuild the exact conditions later. It’s like leaving breadcrumbs, except the birds are on your side this time.

Close-up of network cables and server connections

Step Two: Reproduce the Conditions, Not the Bug

You can’t reproduce an intermittent bug on demand. That’s the whole point. What you can reproduce are the conditions that wake it up. This means building a test setup that mirrors production load, data shapes, and timing. If the failure appears under high concurrency, your test has to generate genuine contention. Synthetic benchmarks that loop over a single request are a waste of electricity. Use production traffic replay tools or generate load with the same statistical distribution of inter-arrival times. Realism matters.

Pay attention to state. Many intermittent failures are state-driven: a cache entry expires mid-request, a file handle gets recycled, a connection pool drains at exactly the wrong moment. Your test environment has to match production state as closely as you can manage. Clone a production database snapshot, anonymize it, and run against that. If the failure involves a race condition, you need a test that exercises the same ordering dependencies. Tools like Chaos Monkey or custom fault injection help, but only if you already have a rough idea of the failure domain. Otherwise, you’re just breaking things for fun.

Using Deterministic Simulation

For gnarly concurrency bugs, I’ve reached for a deterministic scheduler. Frameworks exist that intercept thread scheduling and I/O operations so you can control execution interleaving. You define a set of concurrent operations and the tool explores different orderings until a failure condition pops. This isn’t really a production debugging technique, but it’s often the only way to corner a race condition that shows up once a month. Once you find a failing schedule, you’ve got a permanent reproducer. That’s gold.

Step Three: Narrow the Search Space with Differential Diagnosis

Intermittent failures usually span multiple components. Use differential diagnosis to isolate the layer. Disable suspected features one at a time through feature flags or config tweaks. If the failure vanishes when you turn off the caching layer, you’ve got a lead. Do this carefully in production, obviously. Canary deployments and monitoring the failure rate are your guardrails. And please, don’t change five things at once. You’ll never know which one mattered, and you’ll have learned nothing.

Another approach is to compare two populations: instances that exhibit the failure and instances that don’t. Look for differences in configuration, data volume, upstream dependencies, or even hardware. I once tracked down a memory corruption bug by noticing that failing nodes all had a specific DIMM manufacturer. The OS reported zero ECC errors, but bit flips happened at a temperature threshold that only those DIMMs hit. The lesson: check the physical layer. Not every bug lives in your code. Some of them live in the metal.

Engineer analyzing server logs on multiple monitors

Step Four: Implement a Hypothesis-Driven Fix

Don’t fix a bug you don’t understand. That’s just vandalism. Once you have a strong hypothesis about the root cause, implement the smallest possible change that should prevent the failure, and test it under the reproduced conditions. If the failure rate drops to zero, you’re probably right. If it just changes frequency, your hypothesis is wrong or incomplete. Revert the change and go back to collecting data. No shame in that.

Watch out for Heisenbugs: failures that disappear the moment you add logging or alter timing. These are almost always race conditions where the extra instruction shifts memory layout or scheduling. If you suspect a Heisenbug, switch to non-invasive tracing like eBPF or hardware performance counters. Those tools sample state without modifying execution flow. The overhead is minimal and often fine for production. Traditional logging just teaches the bug to hide better.

Deploying the Fix and Verifying

Deploy the fix behind a feature flag and monitor the failure signature. Use a statistical test to confirm the failure rate actually dropped. A simple chi-squared test on pre- and post-deployment error counts works. Don’t squint at a graph and call it done. Intermittent failures have natural variance, and confirmation bias will mess with your head. Set a threshold: if the p-value is below 0.01 and the effect size is large, proceed to full rollout. If not, dig deeper.

Case Study: The Midnight Timeout

A service at a previous job timed out every night at exactly 2:00 AM UTC. The timeout lasted 30 seconds and recovered on its own. Logs showed a spike in database query duration, but the database itself was practically idle. The failure signature was dead simple: every night, same time, same duration. We instrumented the connection pool and discovered it was being drained and recreated at 2:00 AM because of a scheduled credential rotation. That rotation took 30 seconds, during which all connections were invalid. The fix was to rotate credentials without draining the pool, by accepting both old and new credentials for a grace period. The problem had nothing to do with query performance. It was an operational procedure that nobody had thought to align with application state.

The bigger lesson: intermittent failures routinely cross team boundaries. The credential rotation was owned by the security team, who had zero visibility into application behavior. Debugging required correlating application logs with infrastructure change logs. Build that correlation into your observability stack, or you’ll keep getting paged for things that aren’t your fault, technically.

Long-Term Prevention Strategies

Eliminating intermittent failures isn’t a one-and-done project. It’s an engineering habit. First, mandate that every production error log includes enough context to reproduce the failure offline. That means trace IDs, input parameters (sanitized), and environment metadata. Second, invest in production-like staging environments that receive a subset of live traffic. Shadow traffic catches timing bugs before they reach every user. Third, run chaos experiments continuously. I don’t just mean “kill a pod.” I mean “delay network packets by 100ms for 1% of requests” or “fill the disk to 95%.” Intermittent failures adore edge cases. Feed them regularly so you know what breaks.

Finally, accept that some failures will remain mysterious. That’s not an engineering failure; it’s a property of complex systems. What’s unacceptable is not having the data to diagnose them when they recur. Build your systems so the next intermittent failure leaves a trail you can actually follow. Otherwise, you’re just guessing with a pager in your hand.

FAQ

What’s the first thing to check with an intermittent timeout?

Look at connection pool metrics and thread pool saturation. Timeouts are usually resource exhaustion, not slow processing. Check if the pool size fits peak concurrency and whether connections are leaking. A single leaked connection can block requests intermittently and make you chase ghosts for days.

How do I debug a race condition that only occurs in production?

Log the order of critical operations using a monotonic clock. Timestamp entry and exit of synchronized blocks, database transactions, or message queue ops. Compare those timestamps across threads in the failing request. If you capture a partial ordering, you can often infer the race window. It’s tedious, but it works.

Why do my intermittent failures disappear when I add more logging?

Classic Heisenbug. Extra logging changes timing, memory layout, or instruction ordering. The bug is probably a race condition or use-after-free. Switch to non-invasive tracing like eBPF or hardware watchpoints that don’t alter execution flow. If you’re stuck with logging, keep it minimal and use asynchronous appenders to cut down the timing perturbation.

Can intermittent failures be caused by hardware?

Absolutely. Cosmic rays flip bits in RAM. Failing power supplies cause voltage drops that lead to CPU miscalculations. Network switches with janky buffers corrupt packets under load. If you’ve ruled out software causes and the failure correlates with specific physical nodes, bring in your infrastructure team. Memtest86 and network error counters are your friends. The silicon is not always innocent.