Skip to content

// Resilience & Messaging · 06

Bulkhead

Pool Aleads tono shared resourcesPool B (isolated)

Separate resource pools, so one flooded feature can't sink the ship.

How it fits together

Bulkhead structure«client»StorefrontGET /productGET /searchPricing pool50 in flightqueue 20 (full)Search pool20 in flightqueue 3 of 10«external»Pricing servicep99 3s«external»Search servicep99 120ms1 GET price503 at once2 forward1 GET search2 forward
uses / callsfails / error path· numbered steps replay in order
  • Storefront uses / calls Pricing pool (1 GET price)
  • Pricing pool fails / error path Storefront (503 at once)
  • Pricing pool uses / calls Pricing service (2 forward)
  • Storefront uses / calls Search pool (1 GET search)
  • Search pool uses / calls Search service (2 forward)

Spot it

  • One slow API stalls every other endpoint
  • Thread pool or connection pool is exhausted
  • A reporting job slows down checkout

It costs

Partitioned capacity sits idle in one pool while another is saturated.

In code

public sealed class PricingClient(IPricingApi api){    private readonly ConcurrencyLimiter _bulkhead = new(new ConcurrencyLimiterOptions    {        PermitLimit = 50,        QueueLimit = 20,        QueueProcessingOrder = QueueProcessingOrder.OldestFirst,    });​    public async Task<decimal> GetPriceAsync(string sku, CancellationToken ct)    {        using var lease = await _bulkhead.AcquireAsync(1, ct);        if (!lease.IsAcquired)        {            throw new InvalidOperationException("Pricing is saturated; fail fast.");        }​        return await api.GetPriceAsync(sku, ct);    }}
Go deeperThe full explanation and what to ask for · 1 min readExpand

Named after a ship's watertight compartments: a breach floods one section, not the whole hull. In software the compartments are thread pools, connection pools, concurrency limits, queues or whole deployments.

The classic mistake is a shared resource nobody counted as one: a single database pool, the connection limit of one HTTP client, the thread pool under blocking calls. Size each bulkhead from measured concurrency (throughput × latency), and reject fast when full instead of queueing forever.

Bulkheads contain the damage, circuit breakers stop the calls, timeouts bound how long a slot stays taken. Without a timeout a slow dependency still fills its bulkhead; it just stops spreading.

Ask for it

  • isolation
  • concurrency limiter
  • queue limit
  • resource pool
  • blast radius
  • load shedding

Isolate the pricing and search clients with a concurrency limiter each (pricing 50 in flight, queue 20; search 20, queue 10), rejecting the rest at once with 503. Move the reporting consumer to its own deployment with its own database connection pool.