Web Development · Architecture Patterns
The Bulkhead Pattern: Stopping One Slow Dependency From Sinking Everything
A circuit breaker stops you from calling a failing service. The bulkhead pattern is the piece that comes before that: making sure a slow, not-yet-failing service can't consume every thread and starve every other feature in the meantime.
Prathviraj Singh
5 min read
Sponsored
A payment provider starts responding in nine seconds instead of ninety milliseconds. Nothing has failed yet, every request eventually gets a response, so no circuit breaker trips and no alert fires on error rate. Meanwhile every thread in your shared HTTP client pool is now parked waiting on that nine-second response, and your health check endpoint, which shares the same pool, can’t get a thread to respond to the load balancer. The load balancer marks the instance unhealthy and pulls it from rotation. A single slow dependency just took down an entire unrelated feature, without a single error being thrown anywhere in the chain that would have told you why.
That’s the failure mode the bulkhead pattern exists to stop.
The idea, and where the name comes from
A ship’s hull is divided into watertight compartments, bulkheads, specifically so that a breach in one compartment floods that compartment and stops there. The rest of the ship stays dry and the ship stays afloat, even with one section fully compromised. Software bulkheads apply the same idea to a shared resource, most commonly a thread pool or connection pool: give each dependency, or each critical feature, its own isolated allocation, so that one dependency consuming all of its allocation can’t touch the resources anything else depends on.
Without a bulkhead: With a bulkhead:
┌─────────────────────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ Shared thread pool │ │Payment │ │ Auth │ │Health │
│ (50 threads) │ │pool │ │pool │ │check │
│ │ │(15) │ │(15) │ │pool(5) │
│ Payment API: slow, │ └───┬───┘ └───┬───┘ └───┬───┘
│ consuming 48/50 │ │ │ │
│ threads waiting │ Payment Auth Health
│ │ slow, uses stays check
│ Auth + health checks │ its own 15 fine stays fine
│ starved, 2 threads │ threads,
│ left for everything │ maxes out,
│ else │ doesn't touch
└─────────────────────┘ the other pools
Left side: one shared pool means a slow dependency’s damage is unbounded, up to the entire pool. Right side: each dependency’s damage is capped at its own allocation, and everything else keeps working.
Where this sits relative to a circuit breaker
Circuit breakers and bulkheads get grouped together because they’re both resilience patterns and both commonly ship in the same library (Resilience4j, Polly), but they cover different parts of a failure’s timeline. A circuit breaker trips once a dependency has failed enough times to cross a threshold, then stops calling it for a cooldown period. That’s a real protection, but it depends on failures being visible as failures, actual errors or timeouts, and it does nothing during the window where a dependency is degrading but every call still technically succeeds, just slowly.
That slow-but-not-failing window is where bulkheads earn their keep. A response that takes nine seconds and eventually returns 200 OK never trips a failure-rate threshold. But nine seconds of a thread being occupied, multiplied across concurrent requests, is exactly what exhausts a shared pool. Bulkheads cap the blast radius during that window regardless of whether the dependency ever gets bad enough to trip a breaker. In practice, the two patterns are complementary: the bulkhead limits how much damage a dependency can do while it’s degrading, and the circuit breaker stops calling it once it’s degraded enough to be classified as failing outright.
What implementing one actually looks like
The most common form is a dedicated connection or thread pool per dependency, sized deliberately rather than left to share a single global pool:
// Resilience4j bulkhead, conceptually
BulkheadConfig paymentConfig = BulkheadConfig.custom()
.maxConcurrentCalls(15) // this dependency can never hold more than 15 threads
.maxWaitDuration(Duration.ofMillis(500))
.build();
Bulkhead paymentBulkhead = Bulkhead.of("payment-service", paymentConfig);
Supplier<PaymentResult> decorated = Bulkhead
.decorateSupplier(paymentBulkhead, () -> paymentClient.charge(request));
The auth service, the health check endpoint, and any other outbound dependency get their own Bulkhead instances with their own limits, sized to what each one reasonably needs under normal load. A slow payment API can max out its 15 threads and stay maxed out; it structurally cannot touch the threads reserved for auth or health checks, because those live in separate pools with separate caps.
The same idea applies at the infrastructure layer without touching application code: per-route concurrency limits in an API gateway, or per-service connection limits in a database connection pooler like PgBouncer, achieve the same isolation for database-bound resources instead of thread pools.
When it’s worth the resource cost
Bulkheads aren’t free. A dedicated pool sized for a dependency’s worst-case load sits mostly idle the rest of the time, capacity you’re paying for and not using most days. That’s a real cost, and it means bulkheads aren’t something to apply uniformly to every outbound call a system makes.
They’re worth it specifically for dependencies whose degradation would otherwise take down something unrelated: anything called from multiple critical paths, anything sharing infrastructure with a health check or liveness probe, anything where “this one integration is having a bad day” turning into “the whole service is unhealthy” would be a disproportionate outcome. A single low-traffic feature calling a single non-critical, isolated dependency doesn’t need this. A payment processor, an auth provider, or a shared database connection pool almost always does, because those are exactly the dependencies with the blast radius to take unrelated features down with them. If your team has never mapped which shared pools sit behind which dependencies, that’s the actual audit worth doing before deciding where bulkheads pay for themselves, not a blanket policy applied everywhere at once.
Frequently asked questions
- What is the bulkhead pattern in software architecture?
- A resilience pattern that isolates resources, most commonly thread pools or connection pools, per dependency or per feature, so that one slow or failing component can only exhaust the resources allocated to it, not the resources every other part of the system also needs. It's named after the watertight compartments in a ship's hull that keep a breach in one section from flooding the entire vessel.
- How is a bulkhead different from a circuit breaker?
- They solve adjacent problems at different points in a failure's timeline. A circuit breaker trips after a dependency has failed enough times to cross a threshold, and then stops calling it entirely for a cooldown period. A bulkhead limits how much damage a dependency can do while it's degrading but hasn't failed enough to trip a breaker yet, the period where every call is slow rather than erroring outright, and slow calls are what actually exhaust a shared thread pool.
- Do I need a bulkhead if I already have a circuit breaker?
- Usually yes, because they cover different failure modes. A circuit breaker does nothing to protect you while a dependency is merely slow rather than actively failing, since a slow response that eventually succeeds doesn't trip most failure thresholds. That slow period is exactly when a shared, uncapped thread pool gets consumed by requests waiting on the slow dependency, starving requests to unrelated, healthy services. A bulkhead caps that exposure regardless of whether the slow dependency ever crosses the circuit breaker's failure threshold.
- What does implementing a bulkhead actually look like?
- Most commonly, a dedicated thread pool or connection pool per external dependency, sized to what that dependency can reasonably need, instead of one shared pool serving every outbound call. Libraries like Resilience4j (Java) and Polly (.NET) provide bulkhead implementations as a first-class concept alongside circuit breakers and retries. At the infrastructure level, per-service connection pool limits in a database proxy or per-route concurrency limits in an API gateway serve the same purpose.
- Is the bulkhead pattern worth the resource cost for every dependency?
- No. Reserved, mostly-idle capacity is the real cost, and it's only worth paying for dependencies whose slowness could otherwise take down unrelated features. A background, non-critical integration that only one low-traffic feature depends on doesn't need its own isolated pool. A payment processor, an auth provider, or any dependency called from multiple critical paths is exactly the case bulkheads are for, because that's where a shared pool turns one slow call into an outage for everything else sharing it.
Sources
Sponsored
More from this category
More from Web Development
R.01 Webhook Design: Signatures, Retries, and Idempotency Done Right
R.02 Node.js Is Moving to One Major Release a Year. What That Means for Your Upgrade Plan
R.03 WebMCP: How Chrome Lets a Website Expose Its Own Tools to AI Agents
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored