Retry, Bulkhead & Rate Limiting
Circuit Breaker is Resilience4j's headline pattern, but three quieter ones next to it matter just as much in a real production system. This module is those three.
Learning objectives
- Beginner: Add a @Retry annotation to a call that occasionally fails due to a transient network blip.
- Intermediate: Explain what a bulkhead isolates, and why the name is a nautical metaphor.
- Advanced: Combine retry, bulkhead, and rate limiting with a circuit breaker on the same method without them working against each other.
◆ The problem
Some failures are genuinely transient — a momentary network blip, a downstream service mid-restart for one second — and simply trying again a moment later succeeds. Treating every failure as permanent (failing the whole request immediately) wastes a huge fraction of "failures" that would have succeeded on a second attempt.
@Retry(name = "inventoryService", fallbackMethod = "stockFallback") public StockLevel getStock(String sku) { return inventoryClient.getStock(sku); }
▲ Pitfall
Retrying a call that ISN'T idempotent (see this category's Idempotency module) can cause real damage — retrying a failed "charge this card" call, if the first attempt actually succeeded downstream despite a lost response, charges the customer twice.
◆ Under the hood
The name comes from a ship's bulkheads — physical compartments that keep water from one flooded section spreading to sink the entire vessel. A software bulkhead does the same for THREAD POOLS: giving each downstream dependency its own limited pool of concurrent calls, so a hung payment-service can exhaust ONLY the threads allocated to payment calls, not the threads the SAME application needs to keep serving unrelated inventory-service calls.
resilience4j: bulkhead: instances: paymentService: max-concurrent-calls: 10
💻 Code example
resilience4j: bulkhead: instances: paymentService: max-concurrent-calls: 10
Rate limiting caps how many calls are allowed through in a given time window — used defensively at TWO different points: an API Gateway rate-limits incoming CLIENT traffic (covered in the Gateway module), while a service can ALSO rate-limit its own OUTGOING calls to a downstream dependency that has its own capacity limits, so one aggressive internal caller doesn't overwhelm a shared downstream service that other teams also depend on.
@RateLimiter(name = "inventoryService") public StockLevel getStock(String sku) { return inventoryClient.getStock(sku); }
Resilience4j applies these in a specific, deliberate order when stacked on one method: Retry → Circuit Breaker → Rate Limiter → Bulkhead (closest to the actual call). This order matters — Retry sits OUTSIDE the circuit breaker so that each retry attempt is itself counted toward the circuit breaker's failure rate (letting the circuit correctly trip if retries keep failing), while Bulkhead sits INNERMOST, right next to the actual call, so it limits total concurrent in-flight calls to that dependency including every retry attempt, not just first attempts.
✓ Quick recap
- Retry re-attempts a call assumed to be transient — but only ever safely for genuinely idempotent operations.
- Bulkhead isolates each downstream dependency into its own thread-pool compartment, so one hung dependency can't starve calls to a completely unrelated one.
- Rate limiting protects a downstream dependency from your OWN service's excessive call volume, not just protecting you from clients.
- When combined, the order (retry → circuit breaker → rate limiter → bulkhead) is Resilience4j's deliberate default, not arbitrary.
Want a visual for this concept?
Generate a diagram tailored to “Retry, Bulkhead & Rate Limiting” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →