Skip to content

Bulkhead

Your service talks to several dependencies. A product page, for instance, might call a catalog service, a reviews service, and a recommendations service to assemble one response. All three calls share the same finite pool of resources inside your process: the same thread pool that handles incoming requests, the same database connection pool, the same fixed number of in-flight HTTP connections.

That sharing is invisible and convenient — until one of those dependencies misbehaves.

Suppose the recommendations service — the least important of the three — gets slow. Every request that calls it parks a thread for the full timeout. Because all dependencies draw from one shared pool, those parked threads are not “recommendations threads”; they are the threads, the same ones the catalog and reviews calls need. As recommendations requests accumulate, they consume the entire pool. Now requests for the catalog, which is perfectly healthy, cannot get a thread either. A failure in the least important dependency has taken down the most important one.

This is resource contention through a shared pool: one greedy or stuck dependency starves all the others, and a circuit breaker alone does not fully solve it — there is a window, before the breaker trips, where the shared pool can already be drained.

So the forces are: you have finite resources, multiple dependencies competing for them, and you need one slow dependency’s appetite to be capped so it cannot consume the share that other, healthy work depends on.

A bulkhead partitions resources so that each dependency draws from its own bounded pool instead of one shared pool. The name comes from shipbuilding: a hull is divided into watertight compartments by bulkheads, so a breach in one compartment floods only that compartment and the ship stays afloat. In software, you give each dependency its own thread pool, connection pool, or — most simply — a bounded semaphore that caps how many concurrent calls to that dependency may be in flight at once.

When the recommendations dependency saturates, it can occupy at most its compartment’s permits. Once those are exhausted, further recommendation calls are rejected immediately (and can fall back to “no recommendations”), but the catalog and reviews compartments are untouched. The damage is contained.

flowchart TB
  R[Incoming requests] --> C{Per-dependency<br/>bulkheads}
  C --> CAT
  C --> REV
  C --> REC
  subgraph CAT[Catalog compartment]
    CP[8 permits] --> CS[Catalog service]
  end
  subgraph REV[Reviews compartment]
    VP[4 permits] --> VS[Reviews service]
  end
  subgraph REC[Recommendations compartment]
    RP[2 permits — saturated] --> RS[Recommendations service]
  end
  RP -. "extra calls rejected fast" .-> FB[Fallback: no recs]
Each dependency gets its own bounded compartment; a saturated one rejects fast without touching the others

Here is a bulkhead implemented as a bounded semaphore: at most N calls to a given dependency run concurrently, and a caller that cannot immediately acquire a permit is rejected rather than left to pile up. Each example is self-contained and idiomatic to its language.

class BulkheadFullError extends Error {}
class Bulkhead {
private inFlight = 0;
constructor(private readonly limit: number) {}
async run<T>(fn: () => Promise<T>): Promise<T> {
if (this.inFlight >= this.limit) {
throw new BulkheadFullError('bulkhead full');
}
this.inFlight += 1;
try {
return await fn();
} finally {
this.inFlight -= 1;
}
}
}
// One compartment per dependency — sized independently.
const catalog = new Bulkhead(8);
const reviews = new Bulkhead(4);
const recs = new Bulkhead(2);
const recommendations = await recs
.run(() => recsClient.fetch(productId))
.catch(() => []); // recommendations saturated → degrade, do not block catalog

What you gain:

  • Fault isolation. A slow or failing dependency can consume at most its own compartment’s resources. Healthy dependencies keep their share, so one failure no longer drags down the whole service.
  • Predictable resource use. Each dependency’s maximum concurrency is explicit and bounded, which makes capacity planning and load testing far more tractable — you know the worst case for each compartment.
  • Graceful degradation. When a compartment is full, the call is rejected fast and can fall back, so the user gets a product page without recommendations rather than no page at all.

What it costs you:

  • Sizing each compartment. You now have several pools to tune instead of one. Make a compartment too small and you reject calls a healthy dependency could have served; too large and it stops being an effective boundary.
  • Lower peak utilization. Reserving capacity per dependency means some permits sit idle when their dependency is quiet — you trade a little raw throughput for isolation. That is usually a good trade, but it is a trade.
  • More moving parts. Per-dependency pools add configuration and monitoring surface. Each compartment is worth a metric so you can see which one is saturating.
  • Circuit Breaker — bulkheads contain the damage; a breaker then stops calling the broken dependency entirely.
  • Retry and Timeout — bounds how long each call holds a permit, which keeps a compartment from staying full forever.
  • Rate Limiting — caps the rate of work, where a bulkhead caps the concurrency of work.
What does the bulkhead pattern isolate?
Where does the bulkhead pattern get its name?
How does a bulkhead prevent one slow dependency from taking down a whole service?
How does a bulkhead differ from a rate limiter?