← All packs

Concurrency bug finding: name the interleaving, not "might race"

"Intermittent," "only under load," "only in production," and "only sometimes" are concurrency's telltale signature — and the reason these bugs are structurally different from ordinary logic bugs. The code is often individually correct in isolation and only breaks under a specific interleaving of threads, tasks, or workers. Reviewing for the interleaving, not the symptom, is what finds them. "This might race" isn't a finding; the two-or-more-step sequence across threads that produces the wrong result is.

1. Find every piece of shared mutable state

Start by listing what's actually reachable from more than one thread, task, or worker: module-level or class-level variables, shared caches or maps, shared connections, and external resources — a database row, a file on disk — touched by concurrent handlers. Nothing else in this checklist matters until this list is complete, because every subsequent check is scoped to items on it.

Check: for each shared item, confirm it's actually reachable from more than one concurrent context. State that's per-request or per-thread by construction isn't in scope, no matter how it looks at first glance.

2. Check for unsynchronized read-modify-write

A "check-then-act" — read a value, decide something based on it, then write — is unsafe unless the whole sequence is atomic: a lock, a compare-and-swap, or a database transaction with the right isolation level. Any increment, append-if-absent, or "get or create" pattern touching shared state is a candidate.

Check: for every read-modify-write on shared state from step 1, confirm the whole sequence — not just the write — is provably atomic. A single atomic write after a non-atomic read is still unsafe if another writer can act in between.

3. Check lock discipline, if locks are used

Three failure modes cover most lock bugs:

Check: map every lock acquisition against these three failure modes specifically, rather than confirming only that a lock exists somewhere near the shared state.

4. Check ordering assumptions in async/event-driven code

Concurrency bugs don't require explicit locks — async and event-driven code carries its own ordering assumptions, and they're often false without explicit sequencing:

Check: for each of the three, find the specific line where the assumption is made, not just the general area of the code that looks async-heavy.

5. Check initialization races

A lazy-initialized shared singleton or cache built with a check like if not initialized: initialize() and no lock is a race if it's reachable from concurrent entry points — two callers can both see "not initialized" and both run initialize(), sometimes producing two live instances where the code assumes exactly one.

Check: find every lazy-init pattern on shared state and confirm the check-and-initialize sequence is itself protected, not just the state it produces.

Name the specific interleaving, not just "this might race"

A concurrency finding is only useful if it's falsifiable in principle, even if reproducing it in practice needs load or timing tricks. That means stating the shared state involved, the unsafe operation, and the ordered sequence of steps across the threads or tasks that produces the wrong result — for example: "thread A reads count as 4, thread B reads it as 4 before A writes, both compute 5, both write 5 — one increment is lost" — not "the counter might have a race condition." The first is a report; the second is a hunch wearing a report's clothes.

Don't flag sharing that isn't mutation

Append-only immutable data shared across threads is safe by construction — flagging it as risky just because concurrency is present, without identifying an actual mutation, produces noise that trains reviewers to skim past real findings. The risk is in the mutation, not in the sharing itself; every check above is scoped to state that changes.

Output: state, operation, interleaving, fix

Per finding: the shared state involved, the unsafe operation, the specific interleaving that breaks it (ordered steps across the threads/tasks), and the fix — a lock, an atomic op, restructuring to avoid the shared state, or explicit sequencing. Anything short of that is a lead, not a finding.

None of this requires an LLM on a small, well-understood codebase — it's five checks against a list of shared state you can build by hand. Where a model helps is on a large, unfamiliar one: finding every piece of shared mutable state across a whole codebase (not just the obvious module-level ones), checking each against all five failure modes without fatigue, and stating the actual interleaving instead of stopping at "this might race" — which is what the concurrency-bug-finder skill in the Claude Code Debug Pack does. If the bug turns out to be a single reproducible cause rather than a timing-dependent interleaving, see the companion guide on root-cause tracing. If it traces back to state that grows without bound rather than a race, see the one on memory leak hunting.