What is BlockHound, and how would you use it to catch accidental blocking calls in a WebFlux codebase?
answer
- JVM agent, instruments blocking JDK calls
- Fires only on NonBlocking-marked threads
- Throws BlockingOperationError with stack trace
- BlockHound.install() — usually in tests + JUnit integration
- allowBlockingCallsInside / BlockHoundIntegration SPI
basics
~20 sBlockHound is a Java agent that instruments the JVM to detect blocking calls (like JDBC, Thread.sleep, socket reads) made on threads marked non-blocking — the WebFlux event loop. It throws an error pinpointing the call, so you catch mistakes in tests.
solid answer
~50 sBlockHound is a JVM instrumentation agent (from the Reactor project) that detects blocking method calls executed on threads that must stay non-blocking — Reactor's event-loop and `parallel()` threads, which implement the `NonBlocking` marker. It transforms known-blocking JDK methods (e.g. `Thread.sleep`, socket/file reads, `Object.wait`) so that, when hit on such a thread, it throws `reactor.blockhound.BlockingOperationError` with the exact stack trace. You install it once at startup via `BlockHound.install()` (or `BlockHound.builder()...install()` to customize). Typically you enable it in **tests** — often behind a base test class or a `BlockHoundIntegration` SPI — so CI fails when someone introduces a blocking DAO on the loop. Because some framework blocking is benign, BlockHound supports allow/disallow lists to whitelist specific methods. It's a detection tool, not a fix: it tells you *where* you block so you can offload to boundedElastic or switch to a non-blocking client.
code
java · 22 lines// build.gradle (test scope)
// testImplementation 'io.projectreactor.tools:blockhound-junit-platform:1.0.9'
// Or install manually in a base test class:
class ReactiveTestBase {
@BeforeAll
static void setUp() {
BlockHound.builder()
// whitelist a known-benign framework call if needed
.allowBlockingCallsInside("java.util.UUID", "randomUUID")
.install();
}
}
// A test that will FAIL with reactor.blockhound.BlockingOperationError
// if findById does blocking JDBC on the event loop:
@Test
void endpointDoesNotBlock() {
webTestClient.get().uri("/users/1")
.exchange()
.expectStatus().isOk();
}go deeper
Knows BlockHound detects accidental blocking on the event loop and is used in tests.
Explains it instruments JDK blocking calls, fires on NonBlocking threads, throws BlockingOperationError.
Wires it into CI via the JUnit integration, uses allow-lists, and interprets the stack traces back to JDBC/RestTemplate.
Standardizes a BlockHoundIntegration SPI across services and treats a BlockingOperationError as a build-breaking policy.
**What it is.** **BlockHound** (`reactor.blockhound:blockhound`) is a Java **agent** that uses bytecode instrumentation to make blocking calls *loud*. Reactor already marks its event-loop and `Schedulers.parallel()` worker threads as non-blocking (they implement the `reactor.core.scheduler.NonBlocking` marker). BlockHound goes further than Reactor's built-in `block()` guard: it instruments **low-level JDK methods known to block** — `Thread.sleep`, `Object.wait`, `Socket`/`FileInputStream` reads, `park`, monitor entry, etc. — and inserts a check. If one of those runs **on a non-blocking thread**, BlockHound throws `reactor.blockhound.BlockingOperationError` (an `Error`, so it isn't swallowed by `catch (Exception)`), with a stack trace pointing at the exact offending line. Crucially, this catches blocking that Reactor's own `block()` check misses — e.g. a JDBC driver internally reading a socket, which surfaces as a blocking socket read on the event loop. **Installing it.** ```java BlockHound.install(); // simplest — call once at startup ``` Or customized: ```java BlockHound.builder() .allowBlockingCallsInside("com.example.SomeClass", "someMethod") // whitelist .install(); ``` `install()` must run before the code you want to monitor executes. It's intended to be called once. **Where to use it.** The standard practice is **tests, not production**: - Add `blockhound` (and `blockhound-junit-platform` for auto-install with JUnit 5) as a test dependency. - With the JUnit Platform integration, BlockHound installs automatically for the test run; otherwise call `BlockHound.install()` in a base test class `@BeforeAll`. - Write integration tests that exercise your reactive endpoints/pipelines; if any path blocks on the loop, the test fails with `BlockingOperationError`. This turns 'works in dev, dies under load' into a compile-time-ish guarantee. - Running it in production is possible but adds overhead and can throw on benign framework blocking; most teams keep it test-only. **Allow / disallow lists.** Not all blocking is a bug — some libraries legitimately block during startup or on non-loop threads. BlockHound provides: - `allowBlockingCallsInside(className, methodName)` — whitelist a specific method. - `disallowBlockingCallsInside(...)` — the opposite, to tighten. - A `BlockHoundIntegration` SPI (registered via `META-INF/services`) so libraries (Reactor, R2DBC drivers) can ship their own sensible allow-lists. You implement one to centralize your project's customizations. **Interpreting results.** A `BlockingOperationError` names the blocking JDK call *and* the thread. Trace it up to your code: usually a JDBC/JPA repository, `RestTemplate`, `Thread.sleep`, or a synchronized block reached from a `flatMap`. Fix by (a) switching to a non-blocking client (R2DBC, `WebClient`), or (b) offloading with `subscribeOn(Schedulers.boundedElastic())`. **Gotchas.** - BlockHound needs the JVM to allow instrumentation; on newer JDKs you may need `-XX:+AllowRedefinitionToAddDeleteMethods` or module-access flags depending on version. - It only flags blocking on **non-blocking-marked** threads — blocking on `boundedElastic` is intentionally *not* reported (those threads are meant to block). - It detects blocking, it doesn't measure it — a fast-but-technically-blocking call is still flagged. - False positives on framework internals are handled via allow-lists, not by disabling BlockHound wholesale.
- Why run BlockHound in tests rather than production?It adds instrumentation overhead and throws Errors on any blocking call on a non-blocking thread, including benign framework internals. In tests it fails CI early and safely; in prod it risks crashing requests over harmless blocking and slows the JVM.
- BlockHound flags a socket read inside a JDBC driver. What's really happening?The driver is doing synchronous network I/O on the event-loop thread — the JDBC call blocks the loop. BlockHound surfaces the low-level socket read. Fix by moving to R2DBC or offloading the JDBC call to boundedElastic.
saying these in an interview costs you the question
- Thinking BlockHound fixes blocking rather than just detecting it
- Expecting it to flag blocking on boundedElastic threads (it won't — those are meant to block)
- Believing it must run in production to be useful
- Confusing it with Reactor's built-in block() guard (BlockHound catches far more)