skip to content

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