How do reactive health checks differ from blocking ones, and what happens to a blocking HealthIndicator in a WebFlux app?
answer
- ReactiveHealthIndicator.health() -> Mono<Health>
- AbstractReactiveHealthIndicator.doHealthCheck returns Mono, auto-DOWN on error
- blocking indicators adapted onto Schedulers.boundedElastic
- aggregation + HTTP mapping identical to blocking
- always .timeout(); never .block() on event loop
basics
~20 sIn a reactive (WebFlux) app you implement ReactiveHealthIndicator, whose health() returns Mono<Health> instead of a blocking Health. Existing blocking HealthIndicators still work — Spring adapts them and runs their code on a bounded elastic scheduler so they don't block the event loop.
solid answer
~40 sReactiveHealthIndicator.health() returns Mono<Health>, letting the check compose non-blocking I/O (e.g., a reactive DB ping) without tying up event-loop threads. For convenience there's AbstractReactiveHealthIndicator, whose doHealthCheck(Health.Builder) returns Mono<Health> and which catches errors into DOWN, mirroring the blocking base class. In a WebFlux application Actuator prefers the reactive registry, but any plain blocking HealthIndicator beans are still honoured: Spring wraps them so their blocking health() executes on a bounded elastic scheduler (Schedulers.boundedElastic) rather than a Netty event-loop thread. The reactive composition types are ReactiveHealthContributor and CompositeReactiveHealthContributor. The main gotchas: don't call blocking code inside a ReactiveHealthIndicator, always bound the check with a timeout so a hung dependency can't stall probes, and remember status aggregation and HTTP mapping behave identically to the blocking world.
code
java · 24 linesimport org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.time.Duration;
@Component
public class ReactiveCacheHealthIndicator extends AbstractReactiveHealthIndicator {
private final ReactiveRedisConnection redis;
public ReactiveCacheHealthIndicator(ReactiveRedisConnection redis) {
this.redis = redis;
}
@Override
protected Mono<Health> doHealthCheck(Health.Builder builder) {
return redis.ping() // non-blocking
.map(pong -> builder.up().withDetail("ping", pong).build())
.timeout(Duration.ofSeconds(2)) // bound a hung dependency
.onErrorResume(ex -> // (base class would also DOWN on error)
Mono.just(builder.down(ex).build()));
}
}go deeper
Know that reactive apps use ReactiveHealthIndicator returning Mono<Health> instead of a plain Health.
Explain AbstractReactiveHealthIndicator's Mono-returning doHealthCheck and that reactive/blocking share aggregation semantics.
Describe how blocking indicators are adapted onto a bounded elastic scheduler and the need for timeouts and non-blocking clients.
Reason about event-loop safety, scheduler saturation under probe frequency, readiness-gating design, and separating cheap readiness from deep dependency checks.
## Blocking vs reactive foundations Spring MVC uses a thread-per-request model; blocking a request thread only hurts that request. **Spring WebFlux** runs on a small pool of **Netty event-loop threads**; blocking one of them stalls many concurrent requests. So Actuator provides a **reactive health API** for WebFlux apps. ## ReactiveHealthIndicator ```java public interface ReactiveHealthIndicator extends ReactiveHealthContributor { Mono<Health> health(); } ``` `Mono<Health>` is Project Reactor's container for **0..1 asynchronous value**. Returning a `Mono` means the check is **deferred and non-blocking**: you compose it from reactive clients (`R2DBC`, reactive Redis/Mongo, `WebClient`) so no thread is parked waiting on I/O. ## AbstractReactiveHealthIndicator The convenience base class mirrors the blocking `AbstractHealthIndicator`: ```java protected abstract Mono<Health> doHealthCheck(Health.Builder builder); ``` Its `health()` calls `doHealthCheck`, and any error signal (`onError`) is translated into a **DOWN** with the exception recorded — the same auto-DOWN behaviour, in the reactive domain. Use it so you don't hand-write `onErrorResume`. ## Composition, reactively The contributor tree has reactive twins: - `ReactiveHealthContributor` — marker - `ReactiveHealthIndicator` — leaf - `CompositeReactiveHealthContributor` — branch (`fromMap(...)`) - `ReactiveHealthContributorRegistry` — the registry Actuator walks in WebFlux apps ## What happens to blocking indicators in WebFlux This is the key interview point. A WebFlux app can still have plenty of plain **blocking `HealthIndicator`** beans (yours, or auto-configured ones). Actuator does **not** ignore them: it **adapts** each blocking `HealthIndicator` into a reactive one and runs the blocking `health()` on a **bounded elastic scheduler** — `Schedulers.boundedElastic()` (historically the "elastic" scheduler) — via `subscribeOn`. That keeps the blocking call **off the event loop**. The adapter is applied so both reactive and blocking contributors coexist in the reactive registry. Conversely, in a **Spring MVC** app there is no reactive endpoint; a `ReactiveHealthIndicator` would just be treated as a regular contributor whose `Mono` is subscribed/blocked as needed. The reactive API is really about WebFlux. ## Aggregation and HTTP mapping are unchanged Worst-status-wins aggregation (`StatusAggregator`), the DOWN→503 / UP→200 mapping, `show-details`, and health groups all behave **identically**. Reactivity only changes *how* each leaf is executed, not the semantics of the result. ## Gotchas / principal-level concerns - **Never block inside a `ReactiveHealthIndicator`** (no `.block()`, no JDBC). Blocking-detection tools (BlockHound) will flag it; in production it silently starves the event loop. - **Always add a timeout**: `.timeout(Duration.ofSeconds(2))` so a hung dependency yields DOWN quickly instead of stalling every probe. A slow health check can itself cause a readiness probe to fail and trigger pod restarts. - **Backpressure/parallelism**: composites subscribe to children; a badly-behaved reactive child can delay the whole tree — bound each child. - **Scheduler saturation**: many adapted blocking indicators all hitting `boundedElastic` under frequent probes can exhaust that pool; prefer truly reactive clients for hot checks, and keep probe intervals sane. - **Don't mix concerns**: readiness checks that gate traffic should be cheap and local; deep dependency checks belong in a separate group so a downstream blip doesn't cordon your pod. ## When to use Use `ReactiveHealthIndicator`/`AbstractReactiveHealthIndicator` whenever the underlying client is reactive and the app is WebFlux. If your only client is blocking, a plain `HealthIndicator` is fine — Spring will schedule it safely — but wrap it in a timeout regardless.
- You add a plain blocking HealthIndicator bean to a WebFlux app. Does it block the Netty event loop?No. Actuator adapts blocking HealthIndicators into reactive ones and runs their health() on Schedulers.boundedElastic via subscribeOn, keeping the blocking call off the event-loop threads.
- Why is a timeout inside a ReactiveHealthIndicator operationally important?A hung dependency without a timeout makes the health Mono never complete, stalling readiness probes; the probe times out and orchestrators may cordon or restart the pod. .timeout() converts a hang into a fast DOWN you control.
saying these in an interview costs you the question
- Saying blocking HealthIndicators are ignored or unsupported in WebFlux (they're adapted onto boundedElastic)
- Claiming ReactiveHealthIndicator returns Health (it returns Mono<Health>)
- Calling .block() or JDBC inside a ReactiveHealthIndicator
- Believing reactivity changes status aggregation or the DOWN->503 mapping (it doesn't)
- Omitting a timeout, letting a hung check stall probes