skip to content

What is BlockHound, and how would you use it to catch accidental blocking calls in a WebFlux codebase?

level: seniorimportance: should knowfreq 45%

answer

  1. JVM agent, instruments blocking JDK calls
  2. Fires only on NonBlocking-marked threads
  3. Throws BlockingOperationError with stack trace
  4. BlockHound.install() — usually in tests + JUnit integration
  5. allowBlockingCallsInside / BlockHoundIntegration SPI

basics

~20 s

BlockHound 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 s

BlockHound 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
java
// 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

for a junior

Knows BlockHound detects accidental blocking on the event loop and is used in tests.

for a middle

Explains it instruments JDK blocking calls, fires on NonBlocking threads, throws BlockingOperationError.

for a senior

Wires it into CI via the JUnit integration, uses allow-lists, and interprets the stack traces back to JDBC/RestTemplate.

for a principal

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)

context