skip to content

Blocking the Event Loop

One blocking JDBC or RestTemplate call on an event-loop thread stalls every request that thread was serving, which is why block() is forbidden there and BlockHound exists. Interviewers love it because the failure looks like a mysterious throughput collapse.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

In a Spring WebFlux app, what does it mean to 'block the event loop', and why is it a problem?

level: juniorimportance: must knowfreq 78%

answer

  1. Few threads: reactor-http-nio-* ~ one per core
  2. Parked thread serves nobody
  3. JDBC / RestTemplate / Thread.sleep = blocking
  4. Offload to boundedElastic
  5. Never block() on the loop

basics

~10 s

It means running a slow, waiting operation (like a JDBC query or Thread.sleep) directly on one of WebFlux's few event-loop threads. That thread can't handle other requests while it waits, so throughput collapses.

solid answer

~40 s

Spring WebFlux runs on Netty with a small pool of event-loop threads (named reactor-http-nio-*), typically one per CPU core. These threads are meant to do only fast, non-blocking work and hand tasks back quickly. 'Blocking the event loop' means calling something that parks the thread while it waits — a JDBC query, RestTemplate call, synchronized lock, or Thread.sleep. While parked, that thread cannot process any other request. With only a handful of threads, a few concurrent blocking calls can freeze the whole server: latency spikes, requests queue, and throughput drops far below a traditional thread-per-request model. The fix is to never block on these threads — use non-blocking clients (WebClient, R2DBC), or if you must call blocking code, offload it to a dedicated scheduler like Schedulers.boundedElastic().

code

java · 13 lines
java
// BAD: blocking JDBC call runs on the reactor-http-nio event loop
@GetMapping("/bad/{id}")
public Mono<User> bad(@PathVariable Long id) {
    // jdbcUserDao.findById is synchronous JDBC -> parks the event-loop thread
    return Mono.just(jdbcUserDao.findById(id));
}

// BETTER: offload the unavoidable blocking call onto a worker pool
@GetMapping("/ok/{id}")
public Mono<User> ok(@PathVariable Long id) {
    return Mono.fromCallable(() -> jdbcUserDao.findById(id))
               .subscribeOn(Schedulers.boundedElastic());
}

go deeper

for a junior

Must know: WebFlux has very few threads; blocking one (JDBC, sleep) hurts everyone. Offload with boundedElastic.

for a middle

Should articulate the reactor-http-nio pool sizing and contrast with Tomcat thread-per-request.

for a senior

Explains end-to-end non-blocking (R2DBC/WebClient) as the real fix, offloading as the fallback, and detection.

for a principal

Frames it as an architectural choice: WebFlux only pays off when the whole stack is non-blocking; otherwise MVC is simpler and safer.

**The event loop.** Spring WebFlux's default server is Reactor Netty. Instead of one thread per request (the Spring MVC / Tomcat model), Netty uses a tiny pool of **event-loop threads** — by default roughly one per CPU core — named `reactor-http-nio-1`, `reactor-http-nio-2`, etc. Each event-loop thread multiplexes thousands of connections: it wakes up when a socket has data, does a quick non-blocking slice of work (parse bytes, run a reactive operator, register a callback), and immediately moves on to the next ready connection. This is why a handful of threads can serve huge concurrency — *as long as no single task holds a thread for long*. **What 'blocking' means.** A **blocking** call is one that makes the calling thread wait (park) until an external operation finishes: a synchronous JDBC query (`Statement.executeQuery`), a `RestTemplate` HTTP call, `Thread.sleep`, `Future.get()`, waiting on a `synchronized` monitor or `ReentrantLock`, or synchronous file I/O. During the wait the thread does nothing but occupy a slot. **Why it starves the loop.** If you run a 200 ms JDBC query on `reactor-http-nio-3`, that thread is unavailable for 200 ms. With, say, 4 event-loop threads and enough concurrent blocking requests, all 4 threads park simultaneously — now the server accepts connections but processes *nothing*. Latency for every unrelated request (even a trivial health check) explodes because there's no thread free to run it. Unlike Tomcat, where you'd just add more threads (each cheap-ish but many), WebFlux deliberately has very few threads, so blocking is catastrophic rather than merely wasteful. **How to avoid it.** - **Prefer non-blocking clients end to end:** `WebClient` instead of `RestTemplate`; **R2DBC** (`R2dbcEntityTemplate`, reactive `ReactiveCrudRepository`) instead of JDBC/JPA; reactive Redis/Mongo drivers. - **If blocking code is unavoidable** (a legacy library, blocking JDBC you can't replace), **offload** it to a scheduler designed for blocking work with `.subscribeOn(Schedulers.boundedElastic())` or `Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic())`. That moves the blocking call off the event loop onto an elastic pool of worker threads. - **Never call `block()`, `blockFirst()`, or `blockLast()`** inside a reactive chain running on an event-loop thread — these fully park the thread waiting for the stream to complete and are the single most common WebFlux mistake. **Detection.** The library **BlockHound** instruments the JVM to detect when blocking calls happen on threads that are supposed to stay non-blocking (Reactor marks event-loop and `parallel()` threads as 'non-blocking only'); it throws `BlockingOperationError` pinpointing the offending call. **Gotcha:** blocking is often invisible in low-traffic tests — a single request 'works fine.' The failure only appears under concurrency, which is why teams add BlockHound in tests to catch it early.

  • How is this different from Spring MVC on Tomcat, where blocking JDBC is normal?
    Tomcat uses thread-per-request with a large pool (e.g. 200 threads); a blocked thread only ties up its own request. WebFlux has ~one thread per core, so blocking starves all concurrent requests. WebFlux trades a big thread pool for a tiny one plus non-blocking I/O.
  • Name three concrete operations that block.
    Synchronous JDBC queries (JPA/JdbcTemplate), RestTemplate HTTP calls, and Thread.sleep. Also Future.get(), synchronized/lock waits, and synchronous file I/O.

saying these in an interview costs you the question

  • Thinking WebFlux has a large thread pool like Tomcat so blocking is harmless
  • Believing wrapping blocking code in Mono.just makes it non-blocking
  • Assuming 'it works in my single-request test' proves there's no blocking problem

context

open as a page

Why are block(), blockFirst(), and blockLast() forbidden on event-loop threads, and where can you legitimately call them?

level: middleimportance: must knowfreq 70%

basics

~10 s

block() waits (parks the thread) until the reactive stream finishes. On an event-loop thread that freezes request processing, so Reactor throws an error. It's fine in tests, main(), or plain non-reactive threads.

open as a page

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

level: seniorimportance: should knowfreq 45%

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.

open as a page

You must call a legacy blocking JDBC repository from a WebFlux handler. How do you integrate it without starving the event loop?

level: seniorimportance: should knowfreq 58%

basics

~10 s

Wrap the blocking call in Mono.fromCallable(...) and move it off the event loop with .subscribeOn(Schedulers.boundedElastic()). That runs it on an elastic worker pool built for blocking I/O, keeping the event-loop threads free.

open as a page

Half of a team's WebFlux services quietly wrap every JDBC call in subscribeOn(boundedElastic()). Evaluate this pattern and advise on the architecture.

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Offloading works but concedes WebFlux's main benefit: you're paying reactive complexity while still tying up a thread per DB call. If the whole stack is blocking JDBC, plain Spring MVC (or MVC on virtual threads) is usually simpler and just as scalable.

open as a page