Isolate resources: separate thread pools per service prevent one slow service from taking down others
The Bulkhead pattern isolates resources into separate pools so that a failure in one component cannot cascade and exhaust resources shared by other components. The name comes from the watertight compartments (bulkheads) in a ship's hull -- if one compartment is breached and floods, the bulkhead walls prevent water from spreading to adjacent compartments, keeping the ship afloat.
In software systems, bulkheads partition thread pools, connection pools, or other finite resources on a per-service (or per-consumer) basis. Without this isolation, a single slow or failing downstream service can consume all available threads in a shared pool, causing every other service to starve and the entire system to collapse.
Real-world implementations:
Three types of clients send requests to different backend services through a shared API Gateway: a Product Browser, an Order Client, and a Payment Client.
The gateway receives all incoming requests and routes them to the appropriate isolated thread pool based on the target service. It does not use a single shared thread pool -- each downstream service gets its own.
Each backend service is fronted by a dedicated thread pool with its own capacity and overflow queue:
When all threads in a pool are busy, new requests overflow into the queue. When the queue is also full, requests are immediately rejected with HTTP 429.
Three independent services (Product, Order, Payment), each handling its own domain. A failure or slowdown in one service only affects its own thread pool.
Thread Pool Isolation: Each dependency gets a dedicated thread pool. Requests execute on pool threads, not on the caller's thread. This provides the strongest isolation -- a hung request blocks a pool thread but cannot block the caller's I/O thread. The tradeoff is higher overhead from context switching and thread scheduling.
Semaphore Isolation: A lighter-weight approach that limits concurrency using a semaphore (counter) instead of a separate thread pool. Requests execute on the caller's thread but are bounded by the semaphore permit count. Lower overhead than thread pools, but a hung request blocks the caller's thread. Best suited for fast, high-volume calls where thread pool overhead is not justified.
Process Isolation: The strongest form of bulkheading. Each service runs in its own process (or container) with dedicated CPU and memory limits. Kubernetes resource requests and limits enforce this at the infrastructure level. A memory leak or CPU spike in one service cannot affect another.
Choosing the right pool size is critical:
pool size = requests per second * p99 latency (in seconds) + small buffer.When a thread pool is saturated:
Retry-After header signals when the client should retry.This fail-fast behavior is preferable to letting clients hang, as it frees client-side resources and enables retries with backoff.
Bulkhead and Circuit Breaker are complementary patterns that address different aspects of fault tolerance:
Used together: the bulkhead prevents resource exhaustion while the circuit breaker cuts off a failing service before the bulkhead even fills up. Resilience4j, for example, recommends wrapping calls with both: the bulkhead limits concurrency, and the circuit breaker trips if the service starts failing despite the concurrency limit.