Engineering Atlas › Problem Solving & Incidents (AI era)

The Scientific Method of Debugging

13 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

Observe before you theorise

Start with the symptom stated as a measurement, not a story. "Checkout returns 502 for 0.4% of requests since 14:10 UTC, only on POST, across all regions" is a symptom. "The load balancer is flaky" is a hypothesis wearing a symptom's clothes. Collect the boring facts first: when it started, what changed near then (systems tend to keep behaving the same until a deploy or config push changes them, so recent changes are the first place to look; Google SRE book, ch. 12), and which dimensions (host, version, region, customer) the failures concentrate on. A failure spread evenly across hosts already rules out every single-host cause.

Hypotheses must be falsifiable

A useful hypothesis names a mechanism and predicts something you have not yet looked at. "The new release has a bug" predicts nothing specific. "The new release lowered the HTTP client idle timeout below the upstream's keep-alive timeout, so we reuse connections the upstream has already closed" predicts that failures cluster on connections idle for longer than the new timeout, that they appear only on pods running the new version, and that they vanish if you set the timeout back. Each of those predictions can fail, which is what makes the hypothesis worth testing.

Choose the experiment by information, not by comfort

With five live hypotheses, an experiment that only one of them predicts a positive result for is weak: most outcomes leave four standing. An experiment where the hypotheses disagree half and half removes about half of them whatever the outcome. This is binary search applied to explanations. Cost matters too. Grouping existing logs by host is free; deploying a debug build to production costs an hour and some risk. Prefer cheap and discriminating, then expensive and discriminating, and only last expensive and confirming. Google's SRE book makes the same two points: run the most likely and least risky tests first, and design each test so it rules one group of hypotheses in and another out, while watching for side effects such as extra load or verbose logging that change the behaviour you are measuring (Google SRE book, ch. 12).

Keep a debugging log

Write each step as a line: time, hypothesis, experiment, predicted result, observed result, verdict. A result that kills your favourite hypothesis is not wasted: the same chapter calls negative results conclusive and worth publishing so nobody repeats the experiment. The SRE book asks for clear notes of the ideas you had, the tests you ran and the results you saw (a shared document or chat adds timestamps for free), and for changes made during testing to be recorded so the system can be put back (Google SRE book, ch. 12). It stops you rerunning forgotten experiments, forces the prediction to exist before the result, and becomes raw material for the postmortem and for whoever takes over at 4 a.m.

Know when you are done

You are done when you can explain the mechanism end to end, you can make the bug appear and disappear on demand by toggling the cause, and the explanation accounts for every observation, including the odd ones. A fix that makes symptoms go away without a confirmed mechanism is a hypothesis that has not been tested yet.

Down the stack

Layer 1The contract

The human process promises that each step of the investigation leaves a written record of a falsifiable claim, a predicted outcome, and the observed outcome. Testable consequences: two engineers reading the log should agree which hypotheses are dead, nobody should be able to say "we tried that" without a log line, and the final explanation must predict every recorded observation. When an observation does not fit, the investigation is not finished, however good the fix looks.

Layer 2The mechanism

Experiments are made of instruments, and the most basic instrument is the debugger. A software breakpoint in gdb on x86 works by overwriting the first byte of the target instruction with 0xCC (int3) through ptrace(PTRACE_POKETEXT) (ptrace(2)). When the CPU executes it, the kernel delivers SIGTRAP, the tracee stops, and the debugger is woken from waitpid. To continue, the debugger restores the original byte, rewinds the instruction pointer by one, single-steps with PTRACE_SINGLESTEP (one instruction, then a SIGTRAP), and writes 0xCC back. Hardware watchpoints use the CPU debug registers instead (x86 has four address registers, DR0 to DR3, plus a control register), so you can watch only a handful of addresses at once, but you watch them at full speed. When gdb cannot get a hardware slot (too many watchpoints, or a region wider than the hardware allows), it falls back to a software watchpoint that single-steps the program and can be hundreds of times slower (GDB manual). Conditional breakpoints are evaluated by the debugger after each trap, which is why a conditional breakpoint on a hot loop can slow a program by orders of magnitude even when the condition is almost never true.

Layer 3The machine

strace is built on the same ptrace interface. With PTRACE_SYSCALL, the tracee stops at every syscall entry and exit (PTRACE_O_TRACESYSGOOD only tags those stops so the tracer can tell them from ordinary signals; a seccomp filter with PTRACE_O_TRACESECCOMP can narrow the stops to chosen syscalls), and each stop costs at least two context switches into and out of the tracer (ptrace(2)). On a syscall-heavy process that can slow things down by an order of magnitude or more. Brendan Gregg measured a worst-case dd running about 442x slower under strace, so the figure depends entirely on the syscall rate (Gregg). That overhead changes timing, which is precisely how a race condition stops reproducing while you watch it.

Lower-overhead instruments move the observation into the kernel. perf samples the instruction pointer and stack on a timer interrupt (for example 99 Hz per CPU), so its cost is roughly fixed per sample rather than per event. eBPF programs attached to kprobes, uprobes or tracepoints run in kernel context after the verifier has checked them, aggregate into BPF maps, and only copy summaries to user space. A probe hit typically costs well under a few microseconds, cheap at moderate event rates but noticeable at millions per second.

Why it's built this way

  1. Production systems are larger than anyone's mental model, so you cannot reason your way to the cause; you have to ask the system questions.
  2. Humans read ambiguous evidence as confirmation, so the method forces a written prediction before the result is seen.
  3. Each question costs time and risk, so you rank experiments by how many hypotheses they can kill per unit of cost.
  4. Every instrument perturbs timing and resources, so tools evolved from stop-the-world ptrace towards sampling and in-kernel aggregation, letting you observe without destroying the evidence.

Diagram

flowchart TD
    O["Observe symptom as a measurement"] --> H["List hypotheses with mechanisms"]
    H --> P["Write predictions per experiment"]
    P --> E["Pick cheapest experiment that splits hypotheses"]
    E --> R["Run it and record result"]
    R --> K{"Exactly one survivor that explains all data?"}
    K -->|"no"| H
    K -->|"yes"| T["Toggle cause: bug on, bug off"]
    T --> D["Fix and verify"]

Worked example

Checkout returns 502 on 0.4% of POSTs (about 120 per minute at 30,000 requests per minute) since 14:10. A deploy of the checkout service went out at 14:02. The team has four hypotheses:

HypothesisBy hostBy versionBy idle time of reused connectionRevert client config
H1: idle timeout mismatch with upstreamspreadnew onlyonly idle > 60 sfixes
H2: application bug in new releasespreadnew onlyno patternno effect
H3: one bad nodeone hostbothno patternno effect
H4: upstream overloadspreadbothno patternno effect

Grouping existing logs by host is free and kills H3 if failures are spread. They are: 38 of 40 hosts show errors. Grouping by version is also free. All 502s come from new-version pods, killing H4. Two survive. The idle-time split is the discriminating experiment between H1 and H2, and it needs only a log field the HTTP client already records. Prediction for H1: failures only on connections idle longer than the upstream's 60 second keep-alive. Result: 100% of failures are on connections idle between 60 and 90 seconds, and the release changed the client's idle timeout from 30 to 90 seconds. Final check: set it to 45 seconds on one canary pod and watch its 502 rate drop to zero while its neighbours keep failing. Four cheap experiments and a mechanism that explains everything.

In the wild

Trade-offs

ApproachStrengthsWeaknessesChoose when
Hypothesis-driven experimentsConverges fast, leaves a record, resists biasNeeds discipline, feels slow at firstAny bug that survived a first quick look
Bisecting changesMechanical, needs no theoryNeeds a reliable oracle and a known-good pointRegression with a clear before and after
Asking an agent for hypothesesWide coverage of possible causesPlausible but untested, can anchor the teamGenerating the list, never closing it

Pitfalls in production

Leadership lens

Check yourself

  1. Why is an experiment that only one of five hypotheses predicts often a poor choice?

    Show answer

    Most outcomes leave four hypotheses alive, so the expected information gained is low. An experiment that splits them roughly evenly removes about half whatever the result.

  2. A race condition disappears when you run the service under strace. Why, and what would you try next?

    Show answer

    ptrace stops the process at every syscall and adds context switches, which changes thread interleavings. Use a sampling or in-kernel tool such as perf or bpftrace, or record the execution with a replay debugger.

  3. Why must the prediction be written down before running the experiment?

    Show answer

    After seeing a result, people reinterpret their hypothesis to fit it. A written prediction makes falsification possible.

  4. What is the minimum evidence you want before declaring the root cause?

    Show answer

    A mechanism that explains every observation, plus the ability to make the bug appear and disappear by toggling that cause alone.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →