Introduction
Bulkhead: compartmentalize so one leak does not sink the ship
On a ship, bulkheads limit flooding to one compartment. In software, a bulkhead limits how much of a service's finite concurrency (threads, connections, memory, CPU) any one dependency or workload class may consume. If that compartment saturates, only that class fails fast — critical paths keep their reserved capacity.
Michael Nygard's stability patterns treat bulkheads as first-class isolation, complementary to timeouts and circuit breakers. Breakers stop calling a sick dependency; bulkheads ensure that even while you are still calling (or waiting), you cannot steal the whole process.
The cascade bulkheads prevent (Little's Law)
Checkout service: shared pool of 200 threads. Calls Payment (20 ms) and Recommendations (20 ms). At 500 RPS, healthy in-flight per dependency ≈ 500 × 0.02 = 10 — comfortable.
Recommendations slows to 5 s. Needed concurrency for recommendations alone: 500 × 5 = 2,500. You only have 200 threads. Within a fraction of a second every thread waits on recommendations. Payment is healthy but checkout cannot call it — critical path dies because of an optional feature.
Bulkhead fix: give recommendations a dedicated pool or semaphore of e.g. 40 permits. When it saturates, recommendation calls fail fast or skip; the remaining 160 threads still serve payment and core checkout.
Mechanism: what you actually implement
- Thread-pool isolation (classic Hystrix style): each dependency gets its own pool + queue bound. Saturation → reject.
- Semaphore isolation (Resilience4j and many Go patterns): cap concurrent calls with a weighted semaphore; cheaper context switching, less isolation from thread-kill issues.
- Connection-pool split: separate HTTP/DB pools per dependency so one pool's wait does not block another's connections.
- Queue bounds: unbounded queues defeat bulkheads — they just move the exhaustion to memory.
The cap must be hard. Elastic pools that grow without limit recreate the shared-pool failure.
Decision table
| Situation | Bulkhead? | Notes |
|---|---|---|
| Multiple outbound deps, unequal criticality | Yes | Classic win (payment vs ads vs recommendations) |
| Single dependency only | Usually no | Nothing to isolate from; focus timeout/breaker |
| Multi-tenant noisy neighbor | Yes | Per-tenant concurrency quotas |
| Latency-critical path + batch jobs in same process | Yes | Separate pools so batch cannot stall interactive |
| Capacity already huge vs load | Maybe later | Still wise as guardrail; cost is idle reserved capacity |
When NOT to bulkhead
- One dependency, one fate — partitioning adds knobs without isolation value.
- You "bulkhead" with huge limits (pool of 10,000) — cosmetic only.
- You need strict fairness across thousands of tenants — consider cell architecture / shuffle sharding, not only in-process semaphores.
Cost: reserved capacity idle when healthy; more tuning; risk of over-partitioning (each slice too small → constant reject under normal load).
Escalation: shuffle sharding when tenants ≫ pools
A per-tenant semaphore isolates each tenant but needs one pool per tenant — untenable at thousands of tenants. Shuffle sharding gets most of the isolation with a fixed number of cells. Split the workers into n cells and pin each tenant to a random subset of k cells (its "shard"); route that tenant's work only to those k.
The isolation comes from combinatorics. With n = 8 cells and k = 2 per tenant there are C(8,2) = 28 possible shards, so two tenants land on the exact same pair only 1/28 ≈ 3.6% of the time. One abusive tenant can saturate at most its own 2 of 8 cells (25%); every other tenant keeps at least one clean cell unless it happened to draw that same rare pair — and a client that retries across its k cells rides out the collision. Blast radius drops from "one global pool, everyone down" to "at most a 1/28 pairing, and even then only partially."
Prefer shuffle sharding over per-tenant semaphores when the tenant count far exceeds the pools you can afford to run, and you can tolerate a small collision probability instead of guaranteeing per-tenant isolation. Keep in-process semaphores when tenants are few and you want a hard, exact quota each.
Sizing sketch
For dependency D: permits ≈ ceil(RPS_D × p99_latency_D × safety), then cap by how much of total capacity you are willing to lose if D is sick. Example: optional reco at 500 RPS, p99 50 ms healthy → ~25 permits; set 40 to absorb spikes; never 200.
Pitfalls and detection
- Shared unbounded executor: metrics show thread count rising with one dependency's latency. Fix: split pools.
- Retries into a full bulkhead: retry storms fill the compartment and delay recovery. Fix: fail fast on reject; budget retries globally.
- Queue too deep: latency balloons before reject. Prefer small queue + fast fail.
- Wrong granularity: one bulkhead for all "HTTP out" — same bug as shared pool. Per dependency (or per class).
Alerts: bulkhead reject rate, pool active/max, dependency latency, saturation duration.
Drill ladder
- Q: 200 threads, 500 RPS, dep latency 5 s. Steady-state threads needed? A: L = λW = 500 × 5 = 2,500 — impossible; saturation guaranteed without isolation/load-shed.
- Q: Semaphore vs thread pool bulkhead — one trade-off. A: Semaphore: lighter, caller thread blocks in call; pool: isolates thread use, higher overhead, can time out queue independently.
- Q: Why does circuit breaker not replace bulkhead? A: Breaker opens after failures accumulate; bulkhead protects capacity during the slow phase before/without open, and partitions healthy vs optional deps.
Sources: Nygard, Release It!; Resilience4j/Hystrix isolation models; Little's Law for sizing.
🤖 Don't fully get this? Learn it with Claude
Stuck on Introduction? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Introduction** (System Design) and want to truly understand it. Explain Introduction from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Introduction** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Introduction** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Introduction** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.