
Concurrency Checklist: 12 Questions to Ask Before Shipping Multithreaded Code
The bug you ship is rarely the one you saw in testing. Multithreaded faults have a habit of waiting until a Friday afternoon, when the queue is deep and every core is busy, to appear for the first time. Nothing in the code changed. The timing did.
No checklist makes concurrent code simple. But a short list of questions, asked before the merge rather than after the incident, closes the gap between "it passed" and "it's correct". Here are twelve worth working through.
Shared state: work out what is actually shared
Almost every concurrency bug starts with an assumption. Someone believes a field is local to one request when it is really held by a long-lived object that every thread can reach.
- 1. Which state is genuinely shared? Trace the object graph. Which instances outlive a single call, and who can touch them? Write the answer down if it isn't obvious; that alone catches a lot.
- 2. Is every mutable shared field protected? There are three safe options: make it immutable, confine it to one thread, or guard it with synchronisation. A mutable, shared, unsynchronised field is not a style preference, it is a defect waiting for load.
- 3. Are compound operations treated as one unit? Check-then-act is the classic trap: reading a map to see whether a key exists, then writing it, is two operations with a gap in the middle. So is incrementing a counter field. Use the atomic type, or hold the lock across both steps.
Locking: short sections, consistent order
Locking is where good intentions go wrong. These three questions catch most of the damage.
- 4. Is there a documented lock order? Deadlocks rarely come from a single lock. They come from two threads acquiring the same pair in opposite orders: account then ledger, ledger then account. Agree on an order, write it in a comment near the declarations, and keep to it.
- 5. Do you call out while holding a lock? Callbacks, events, log sinks, network calls, anything that might take a second lock. Under contention this turns a tidy critical section into a deadlock generator or a latency cliff. Copy what you need, release the lock, then call.
- 6. What happens when the lock is contended? Prefer a bounded queue that rejects work over one that grows until the process dies. Give lock acquisition a timeout where you can, and treat the timeout as a failure you log rather than a retry you loop.
Cancellation and shutdown
Cancellation is a feature, not an afterthought. If a thread cannot be stopped, every deploy becomes a gamble.
- 7. Can the work actually be stopped? Every long-running loop needs to check something: an interrupt flag, a cancellation token, a volatile boolean. Loops that poll queues, retry with backoff or walk large collections are the ones that matter. Make the check cheap and place it where the loop does its real work.
- 8. Who shuts the threads down, and how do you know it worked? Shut pools and schedulers down in the reverse order they were started, wait for termination with a timeout, and log it when the wait expires. A silent shutdown that leaves tasks half-finished is worse than a noisy one.
Failure paths
Concurrency changes how failures behave. A single-threaded error stops one operation; a concurrent one can leave the system in a state nobody designed.
- 9. What happens when one thread throws? A failed task can leave shared state half-updated and an invariant broken. Build the new value first, then publish it in a single step, and use finally blocks to restore anything that must always hold.
- 10. Will anybody see the exception? Exceptions inside a future surface only when someone asks for the result, and uncaught exceptions in a bare thread disappear into a handler that may do nothing at all. Set an uncaught exception handler on your pools, and check the futures you care about.
Testing and observability
You cannot debug an interleaving that happened once, so aim to make races either reproducible or visible.
- 11. Have you tried to break it on purpose? Use latches and barriers to force two threads to meet at the worst possible moment. Run the suite with more threads than cores, and run it repeatedly. A stress test that finds nothing has not proved correctness, but it does make the obvious races show up before your users do.
- 12. Could you diagnose this at three in the morning? Name your threads after the work they do. Log when a lock wait exceeds a threshold. Expose queue depth and pool saturation. A thread dump should tell you who is blocked, on what, and for how long.
Before you ship
Run the twelve questions past the code as a final pass. If you cannot answer one, treat that as a risk rather than a gap in the checklist, and either fix it or write down why it is acceptable for now. Where the concurrency logic is subtle, get a second pair of eyes; a reviewer who has not seen the design will spot a missing lock faster than the person who wrote it.
Keep the answers with the code, in a comment or a short note on the pull request, so the next person to touch the locking order knows what was intended and why. None of this removes the difficulty of concurrent code. It just means the awkward questions get asked at your desk, with your coffee, rather than by a pager.
Photo: MART PRODUCTION / Pexels
Designing Backpressure into Event-Driven Systems
Queues, rate limits and load shedding are the three levers that stop a busy producer overwhelming a slower...
Debugging Race Conditions with ThreadSanitizer: A Practical Walkthrough
ThreadSanitizer catches data races that only appear under load. Here is how to build with it, read its...




