Circuit Breaker
Stops calls when a dependency is failing — bulkhead limits concurrent calls regardless of health. Circuit Breaker
The bulkhead pattern takes its name from the watertight compartments in a ship’s hull — if one compartment floods, the others remain intact and the ship stays afloat. In software, a bulkhead isolates one component’s resource consumption so that its failure or slowdown cannot exhaust shared resources and cascade to unrelated parts of the system.
The most common implementation uses concurrency limits (semaphores) to cap how many concurrent requests or goroutines can be active for a given operation. If payment processing slows down and its bulkhead is full, new payment requests are rejected immediately — they do not block goroutines that could be handling unrelated requests. The order listing endpoint keeps working even as the payment system degrades.
Michael Nygard introduced the pattern in “Release It!” (2007) alongside circuit breakers and timeouts as a set of stability patterns for distributed systems. Bulkheads address the failure mode of resource exhaustion — where a slow dependency ties up goroutines, connection pool slots, or file descriptors until the service is unable to handle any request.
A semaphore-based bulkhead in concept:
Bulkhead(maxConcurrent=10): Request arrives → try to acquire semaphore slot Slot available → proceed, release on completion No slot available → reject immediately (return 503) or queue with timeoutThe rejection is intentional and fast. A rejected request returns a clear error; a blocked goroutine wastes a resource. Under load, fast rejection preserves capacity for requests that can succeed.
Go’s sync.Semaphore (or a buffered channel used as one) is the natural implementation. verikt’s bulkhead capability scaffolds a reusable semaphore-based limiter:
type Bulkhead struct { sem chan struct{}}
func New(maxConcurrent int) *Bulkhead { return &Bulkhead{sem: make(chan struct{}, maxConcurrent)}}
func (b *Bulkhead) Do(ctx context.Context, fn func() error) error { select { case b.sem <- struct{}{}: defer func() { <-b.sem }() return fn() case <-ctx.Done(): return ctx.Err() default: return ErrBulkheadFull }}The default case provides immediate rejection when all slots are occupied.
The bulkhead capability scaffolds the semaphore concurrency limiter with timeout support. It suggests observability (to track bulkhead saturation metrics) and circuit-breaker (to stop calls entirely when the downstream is known to be failing). See the Capabilities Matrix.
Circuit Breaker
Stops calls when a dependency is failing — bulkhead limits concurrent calls regardless of health. Circuit Breaker
Rate Limiting
Rate limiting controls request throughput; bulkhead controls concurrent resource consumption. Rate Limiting
OpenTelemetry
Metrics on bulkhead saturation are critical — you need visibility to tune limits correctly. OpenTelemetry