# 4. @ConcurrencyLimit vs Resilience4j's Bulkhead [Previous: 03-spring-retryable.md](03-spring-retryable.md) | [README](../README.md) | Next: [05-production-checklist.md](05-production-checklist.md) Source: [`ConcurrencyLimitedService.java`](../src/main/java/com/ankurm/resilience/springresilience/ConcurrencyLimitedService.java), [`R4jBulkheadService.java`](../src/main/java/com/ankurm/resilience/r4j/R4jBulkheadService.java). Tests: [`ConcurrencyLimitTest.java`](../src/test/java/com/ankurm/resilience/springresilience/ConcurrencyLimitTest.java). Transcripts: [`04a`](output/04a-concurrencylimit-block.txt), [`04b`](output/04b-concurrencylimit-reject.txt), [`04c`](output/04c-r4j-bulkhead-comparison.txt). Both cap concurrent invocations of one method at 2. Both were hit with 4 concurrent callers, each holding its slot for 300ms, in the same test run. ## BLOCK: queue, no timeout `04a-concurrencylimit-block.txt`: all 4 callers succeed. Sorted completion times were `[300, 300, 597, 600]` ms — two callers finish almost immediately, the other two only after a slot frees, for a total wall time of ~601ms instead of the ~300ms it would take with no limit at all. There is no attribute on `@ConcurrencyLimit` to bound how long a blocked caller waits; it queues until a slot opens, however long that takes. ## REJECT: fail fast, no queue `04b-concurrencylimit-reject.txt`, `policy = ConcurrencyLimit.ThrottlePolicy.REJECT`: 2 callers succeed in ~300ms, and the other 2 are rejected in ~0ms — not made to wait at all. The exception is `org.springframework.resilience.InvocationRejectedException`, found with `javap` on `ConcurrencyLimitBeanPostProcessor$RejectingConcurrencyThrottleInterceptor.onAccessRejected` — it is not documented on the `@ConcurrencyLimit` annotation itself. ## What Resilience4j's Bulkhead adds: a bounded wait `04c-r4j-bulkhead-comparison.txt` runs the identical 4-caller/300ms scenario against a Resilience4j `@Bulkhead(maxConcurrentCalls=2, maxWaitDuration=100ms)`. Two callers finish normally around 300ms; the other two wait up to `maxWaitDuration` (observed: 111ms and 117ms, close to the configured 100ms) and are rejected via the fallback method — a third option that sits between Spring's BLOCK (wait forever) and REJECT (never wait): **wait, but only briefly**. Neither `@ConcurrencyLimit` policy offers that middle ground.
Picking between them. If you want callers to queue briefly rather than either wait forever or fail instantly, Resilience4j's Bulkhead with a short maxWaitDuration is still the only one of the three that does it. If unlimited blocking is actually fine — e.g. limiting concurrent writers to a single-threaded resource — @ConcurrencyLimit's BLOCK policy is one annotation and one less dependency.
Next: [05-production-checklist.md](05-production-checklist.md).