Blocking Calls in Reactive Pipelines
Blocking operations can occupy reactive worker threads while waiting on I/O, reducing concurrency and potentially exhausting the resources that drive the pipeline. In Reactor, blocking work should generally be avoided or explicitly isolated on an appropriate scheduler.
Reactive pipelines are designed to keep a small number of threads busy by representing asynchronous work as signals rather than making threads wait. A blocking call such as JDBC access through a blocking driver, `Thread.sleep`, or a synchronous HTTP client holds the executing thread until the operation completes.
This is especially problematic when blocking code runs on event-loop or other shared reactive worker threads. A few slow operations can consume those threads, preventing unrelated requests and signals from being processed promptly. Under load, this can produce latency increases and thread starvation even when CPU usage is modest.
Reactor provides schedulers intended for different execution characteristics. `Schedulers.boundedElastic()` is commonly used to isolate unavoidable blocking work because it permits a bounded amount of thread growth. The placement of `subscribeOn` or `publishOn` matters: `subscribeOn` affects where subscription and upstream work begin, while `publishOn` changes execution for downstream operators after its boundary.
The preferred solution is often to replace the blocking dependency with a genuinely non-blocking API rather than merely moving the blocking call to another scheduler. If blocking is unavoidable, isolate it deliberately, keep the blocking section small, and monitor latency, scheduler utilization, and resource limits.
Key points
- Avoid blocking calls on event-loop or shared reactive worker threads.
- Use non-blocking libraries when an appropriate asynchronous API exists.
- Isolate unavoidable blocking work with an appropriate scheduler such as `boundedElastic`.
- `subscribeOn` and `publishOn` affect different portions of a Reactor pipeline.
- Scheduler isolation does not make a blocking API intrinsically non-blocking.
Interview questions
Why are blocking calls dangerous in a reactive application?
A blocking call occupies the current thread until the operation finishes. Reactive systems often rely on a relatively small number of threads, so blocking one can delay unrelated work. If enough requests block simultaneously, the application can suffer thread starvation and increased latency. This defeats much of the concurrency benefit of reactive processing.
How would you safely call a blocking API from a Reactor pipeline?
First, I would check whether a non-blocking replacement exists, because scheduler isolation does not change the underlying API's blocking behavior. If blocking is unavoidable, I would wrap the operation in a deferred publisher such as `Mono.fromCallable` and use `subscribeOn(Schedulers.boundedElastic())` to move its execution away from reactive event-loop threads. I would also keep the blocking section narrow and monitor resource usage under realistic load.
What is the difference between `publishOn` and `subscribeOn` when dealing with blocking work?
`subscribeOn` affects the execution context used when subscription propagates through the upstream portion of the pipeline, so it is commonly used to move a source or blocking callable onto a suitable scheduler. `publishOn` changes the execution context for operators downstream of that point. For a blocking source created with `fromCallable`, `subscribeOn` is usually the direct way to control where the callable executes.
Why doesn't `Mono.fromCallable(() -> blockingCall())` by itself solve blocking?
`fromCallable` makes the invocation lazy and represents its result as a reactive signal, but the callable still executes synchronously on whichever thread performs subscription unless its execution is shifted. If subscription happens on an event-loop thread, the blocking call can still block that thread. Therefore, an appropriate scheduler boundary is required when the callable is genuinely blocking. The wrapper changes how the result is represented, not the behavior of the underlying operation.
Would you always use `boundedElastic` for blocking operations?
No. It is a useful isolation mechanism for unavoidable blocking work, but it should not become a substitute for non-blocking I/O. A high-volume blocking dependency can still consume the scheduler's bounded capacity and create queuing or latency. I would first look for a reactive or asynchronous client, then isolate only the unavoidable blocking portions and size and monitor the system based on actual workload.