Concurrency Cheat Sheet

Need X → use Y, with the reason and the price. Every row carries a cost column on purpose: concurrency is always bought with complexity, and a recommendation that does not say what it costs is not a recommendation.

Choosing a model

NeedUseWhyCost
The work is waiting on network or diskAsync tasks on one thread, with a concurrency limitWaiting costs a suspended task, not a thread stack — thousands overlap on one coreEvery CPU-heavy line in a handler now stalls everyone else on that loop
The work is burning CPUProcesses, or threads in a language that runs them in parallelOnly real parallelism reduces wall-clock time for compute; async just interleaves itCopying data across a process boundary, and a startup cost per worker
Mixed: heavy compute inside a request handlerAsync at the edge, a bounded worker pool behind itKeeps the loop free for I/O while compute runs somewhere it cannot block anyoneTwo schedulers to reason about, and a queue between them that can fill
CPython and the work is compute-heavyProcesses (or a native extension that releases the GIL)CPython threads do not execute Python bytecode simultaneously; processes doSerialization on every argument and result, and per-process memory
JavaScript and the work is compute-heavyWorker threads (Node) or web workers (browser)The main loop is single-threaded; workers are the only way to get another coreMessage-passing only — no shared objects unless you reach for SharedArrayBuffer
Fewer than a few hundred milliseconds of total workDo it sequentiallyBelow the coordination overhead, concurrency is pure loss and pure riskYou will be asked why it is "not parallel"; the answer is a measurement
Strong isolation between units of workSeparate processesA crash, a leak or a runaway allocation stays inside one address spaceIPC, slower startup, and no cheap sharing of large data

Shared state

NeedUseWhyCost
Two tasks touch the same variableFirst ask whether they need to at allThe cheapest synchronization is the sharing you removedCopying or partitioning costs memory, and sometimes a merge step
The value never changes after constructionMake it immutable and publish it safelyReaders of an immutable object need no synchronization at allEvery update allocates a new object; churn moves to the allocator and GC
Mostly reads, occasional whole-state replacementCopy-on-write snapshot swapped behind one atomic referenceReaders never block and never see a half-written stateWrites copy the whole structure, and readers may hold a stale snapshot
You cannot say what "correct" means hereStop and write the invariant as a sentenceA primitive chosen without an invariant protects a region, not a propertyIt is slow, unglamorous thinking, and it usually shrinks the design
"Is this a race?"Enumerate interleavings at the granularity of individual reads and writesA race is found by exhibiting a schedule, not by staring at the codeTedious, and the interesting schedules are rarely the first ones you try
Unsynchronized access in C++ where one side writesFix it — this is a data race, not just a race conditionA data race is undefined behaviour: the compiler may assume it cannot happenThe fix is a lock or an atomic, and both cost something on the read path
count += 1 from two tasksAn atomic increment, or a lock around the whole updateRead-modify-write is three steps; the increment you lose is one that interleavedAn atomic is one operation only — it does not protect the next line

Synchronization

NeedUseWhyCost
One invariant spanning several fieldsA single mutex covering exactly those fieldsMutual exclusion is what keeps a multi-field update from being observed half-doneEvery reader now waits, and every path through the region is serialized
A lock is held for a long timeShrink the region: compute outside, mutate insideCritical-section length is the multiplier on every waiting threadMore code runs unsynchronized, so the boundary needs more care
Locks held across an await or an HTTP callNever do this — release before the I/O, revalidate afterA lock held for a network round trip converts a fast path into a queueRevalidation after reacquiring is extra code, and it is easy to get wrong
Many readers, rare writers, long read sectionsA read/write lockReaders proceed together; only writers excludeMore state, worse worst-case, and writer starvation unless the lock is fair
Cap how many things do X at onceA semaphore with N permitsA semaphore counts a resource; a mutex only says "one"A permit not released on the error path leaks capacity permanently
Wait until a condition becomes trueA condition variable, waited on inside a `while` loopWakeups are hints, not proofs — the predicate must be recheckedSignal before wait and the waiter sleeps forever: a lost wakeup
"It woke up but the condition was false"Recheck the predicate and wait again — that is the contractSpurious wakeups are permitted by the platform, not a bug in your codeThe loop is mandatory boilerplate on every wait site
Recursive call re-enters a locked regionRestructure so the lock is taken once, at the boundaryA non-reentrant mutex deadlocks against itself; a reentrant one hides the design flawSplitting locked and unlocked variants of a function duplicates signatures

Queues & backpressure

NeedUseWhyCost
Fast producer, slow consumerA bounded queue between themThe bound is what turns "unlimited memory growth" into "the producer waits"The producer now blocks, so it needs an answer for what to do while blocked
The queue is unbounded "for safety"Bound it and decide the overflow policy explicitlyUnbounded means the failure mode is an OOM at 3 a.m. instead of a rejection nowSomeone must choose block, drop-oldest, drop-newest or reject — and own it
Handing work between stagesA channel, sized deliberatelyOwnership transfer replaces shared mutable state with a copy and a handoffCopying cost per item, and a fixed pipeline shape that is harder to change
Independent entities with their own stateAn actor with a mailboxOne actor processes one message at a time, so its state needs no lockEverything becomes asynchronous, and a slow actor is now a hidden bottleneck
Avoid locks entirely for a shared structureGive one owner the data and send it messagesNo sharing means no interleaving to reason about — the invariant is localLatency of a round trip, and the owner is a serialization point by design
Shut down while work is in flightStop intake, drain the queue with a deadline, then cancel the restOrdering matters: closing the input first is what makes draining finiteA drain deadline means some work is abandoned; it must be safe to retry
Queue depth looks fine but latency is badMeasure queue *age*, not depthA steady depth with rising age means arrivals exceed service rateAge requires an enqueue timestamp on every item

Async

NeedUseWhyCost
Awaiting independent calls one after anotherStart them all, then await the collectionSequential awaits sum latencies that could have overlappedNow all of them run at once — against a dependency that may not want that
Promise.all over 10,000 itemsA bounded map with a concurrency limitPromise.all starts everything immediately; the array is a schedule, not a limitA limit means the total takes longer — deliberately
One rejected promise in an allDecide between all-or-nothing and per-item resultsPromise.all rejects on the first failure while the others keep running unwatchedCollecting settled results means handling a mixed success/failure array
A handler does 200ms of JSON workMove it to a worker, or chunk it and yieldOn a single event loop, that 200ms is added to everyone else's latencyWorkers add serialization; chunking adds interleaving you must make safe
"Make it async so it runs in parallel"Say which core the second thing would run onAsync gives overlap of waiting, not simultaneous execution of computationNone — but the conversation is where the misunderstanding usually surfaces
Async code has to hold a lockUse an async-aware lock, never a blocking oneA blocking mutex on an event loop parks the thread that would run the holderAsync locks are slower and do not protect against blocking code elsewhere
A task nobody awaitsGive it an owner with a scope, or do not start itAn unawaited task swallows its exception and outlives the request that made itScoping means the parent waits, which sometimes is exactly what you were avoiding

Pools & limits

NeedUseWhyCost
Spawning a thread per unit of workA pool with a fixed size and a queueThreads are a bounded resource; a pool makes the bound explicit and visibleWork now queues, and the queue needs a bound and a policy of its own
"How many threads should the pool have?"Derive from what each task waits on, then measureThere is no universal formula — it depends on wait fraction, memory and downstream limitsMeasuring means load-testing with a realistic mix, which takes real time
All workers busy, work still arrivingDecide: queue, shed, or scale — before it happensSaturation is not a bug; the absence of a policy for it isShedding returns errors to users; queueing raises latency for everyone
Uneven task durations across workersA work-stealing schedulerIdle workers take from busy queues instead of waiting for a rebalanceSteal attempts contend, and locality suffers when a task moves cores
Client concurrency exceeds server capacityBound it on the client side tooSending 500 concurrent requests to a service sized for 50 makes both sides slowerYou give up peak throughput you could not actually sustain anyway
A pool per subsystem, all sharing a databaseAdd up the limits before trusting any of themFour pools of 25 is 100 connections against a database configured for 60Central accounting of limits is organizational work, not just code
A cache entry expires and 1,000 callers missCoalesce them into one in-flight computationSingle-flight turns a stampede into one call plus 999 waitersThe waiters share a fate: one slow computation delays all of them

Parallel decomposition

NeedUseWhyCost
A big independent computation over a collectionSplit, run, combine — fork/joinIndependence is the whole precondition; if it holds, the decomposition is mechanicalSplit and merge are real work, and they are on the critical path
Combining results from parallel chunksA reduction with an associative operatorAssociativity is what lets the tree be reordered without changing the answerFloating-point addition is not associative — the result changes with the shape
"How much faster can this get?"Measure the serial fraction firstAmdahl: 5% serial caps you at 20x no matter how many cores you buyThe measurement itself takes effort, and the answer is often disappointing
Tasks depend on each otherDraw the dependency graph and find the spanThe longest chain is the floor on runtime regardless of core countBuilding the graph forces you to make implicit ordering explicit
The same arithmetic on many numbersSIMD, or a library that already uses itOne instruction over a vector is parallelism inside a single coreBranchy or irregular data defeats it; alignment and layout become your problem
Stages that must run in order, on a streamA pipeline, one worker per stageDifferent items occupy different stages at the same timeThroughput is set by the slowest stage, and latency per item goes up
Parallelising it made it slowerCompare the chunk size against the coordination costBelow a threshold, split, schedule and merge cost more than the workFinding the threshold is a measurement per machine, not a constant

Failure

NeedUseWhyCost
Two locks taken in two ordersImpose a global lock order and document itRemoving circular wait removes deadlock structurally, not probabilisticallyThe order becomes a codebase-wide invariant that every new lock must respect
Cannot impose an orderUse try-lock with a timeout and a full backoff-and-retryBreaking hold-and-wait is the other structural fix available to youYou must be able to release everything and redo the work — some code cannot
Everything is running, nothing progressesLook for livelock: retries that keep collidingThreads that all back off identically re-collide forever — that is not a deadlockThe fix is randomized backoff, which makes timing non-reproducible
One thread never gets the lockUse a fair queueing lock — and accept it is slowerBarging locks favour whoever is cache-hot, which starves the unluckyFairness costs throughput; the handoff prevents the fast path
A low-priority holder blocks a high-priority waiterPriority inheritance, if the platform offers itThe holder must be able to finish, so it needs the waiter's priorityPlatform-specific and hard to observe; not available in most runtimes
Threads pile up behind one lockSplit the lock, shard the data, or remove the sharingA convoy forms when service time under the lock exceeds the arrival gapSharding multiplies locks and reintroduces multi-lock ordering problems
Adding threads made throughput dropCount runnable threads against coresPast saturation, extra threads add context switches and cache pollution, not workReducing thread count feels like giving up capacity; the graph says otherwise
A tight loop polls a flagBlock on a condition variable insteadBusy-waiting burns a core to save a few microseconds of wakeup latencyBlocking costs a syscall and a wakeup on every handoff

Debugging

NeedUseWhyCost
The bug vanishes when you add loggingTreat it as a heisenbug: change the schedule deliberatelyLogging changes timing, so the disappearance is evidence, not a cureReproducing needs stress and injected delays, which take time to build
Suspected data race in C++ or GoRun it under a race detector in CIDetectors find unsynchronized conflicting accesses on paths that did executeLarge slowdown and memory overhead, and no proof about paths not taken
Everything is slow, CPU is lowLook at lock wait time, not utilizationWaiting does not show up as CPU — the queue is invisible in a CPU graphYou must instrument acquisition sites to get the number at all
The process is wedgedTake a thread dump and build the wait-for graphA cycle in the graph is a deadlock; you can read it off the stacksDumps are a point-in-time sample and can be huge and hard to read
An async runtime is wedgedDump pending tasks and their await pointsAsync stalls have no thread stacks to read — the task inventory is the evidenceRequires runtime support and instrumentation you may have to add first
A rare interleaving must be reproducedStress test with injected delays and randomized schedulingNormal runs explore a narrow slice of the schedule spaceA green stress run proves nothing about the schedules it did not try
Need the exact failing run backRecord and replay it deterministicallyDeterministic replay makes a timing bug into an ordinary debugging sessionRecording overhead, and tooling that only exists on some platforms
Nothing tells you concurrency is degradingEmit in-flight count, queue depth, queue age and lock wait as first-class metricsThese four move before the outage does; latency moves afterInstrumentation cost, and four more series to store and alert on