skip to content

Non-Blocking Runtime & Threading Model

How WebFlux actually runs: the Reactor Netty event loop, the contrast with thread-per-request, what happens when you block, and the DispatcherHandler pipeline. Interviewers ask so they can find out whether you understand the runtime or just the syntax.

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

explore

questions

20

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

What is DispatcherHandler in Spring WebFlux, and what role does it play in request handling?

level: juniorimportance: must knowfreq 70%

basics

~20 s

DispatcherHandler is the central entry point in Spring WebFlux. For each request it finds the right handler (a controller method), invokes it, and turns the result into an HTTP response. It's the reactive equivalent of DispatcherServlet.

open as a page

What server runs a Spring WebFlux application by default, and what kind of threads handles incoming requests?

level: juniorimportance: must knowfreq 70%

basics

~10 s

By default WebFlux runs on Reactor Netty, an async non-blocking server. Requests are handled by a small pool of event-loop threads named reactor-http-nio-* instead of one thread per request.

open as a page

What is the difference between the thread-per-request model of Spring MVC and the event-loop model of Spring WebFlux?

level: juniorimportance: must knowfreq 85%

basics

~20 s

Spring MVC gives each request its own thread that stays busy until the response is sent, so it uses many threads. WebFlux uses a few event-loop threads that handle many requests by never blocking and switching between them.

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

Explain the roles of HandlerMapping, HandlerAdapter, and HandlerResultHandler and how DispatcherHandler chains them.

level: middleimportance: must knowfreq 60%

basics

~10 s

HandlerMapping finds which handler matches the request. HandlerAdapter knows how to call that handler and returns a HandlerResult. HandlerResultHandler takes that result and writes the HTTP response. DispatcherHandler runs them in that order.

open as a page

Why must you never run blocking code on a reactor-http-nio thread, and what do you do when you have an unavoidable blocking call?

level: middleimportance: must knowfreq 75%

basics

~10 s

Event-loop threads are few and shared across many connections. Blocking one freezes every request it serves. Offload blocking work to a separate scheduler with .subscribeOn(Schedulers.boundedElastic()).

open as a page

Why does the event-loop model scale better than thread-per-request under high concurrency with slow I/O, and where does thread-per-request hit its wall?

level: middleimportance: must knowfreq 75%

basics

~20 s

In thread-per-request, each slow call ties up a whole thread doing nothing but waiting, so once all threads are waiting no new requests can be served. Event-loop threads release themselves during waits, so a few threads serve thousands of waiting requests.

open as a page

Why does ThreadLocal-bound context (SecurityContextHolder, MDC, transaction context) break under WebFlux's event-loop model, and how does Reactor solve it?

level: seniorimportance: must knowfreq 65%

basics

~20 s

ThreadLocal stores data on a specific thread, and the thread-per-request model keeps one thread for the whole request. In WebFlux a request hops across event-loop threads, so ThreadLocal values are lost. Reactor replaces it with a Context carried inside the reactive chain.

open as a page

What is ServerWebExchange, and how does it differ from the servlet request/response model?

level: middleimportance: should knowfreq 50%

basics

~10 s

ServerWebExchange is WebFlux's per-request container. It holds the reactive request (ServerHttpRequest) and response (ServerHttpResponse) plus attributes and session. Unlike servlets, bodies are reactive streams (Flux<DataBuffer>), not blocking InputStreams.

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

Trace a request from the reactive server down to the controller: how do HttpHandler, WebHandler, WebFilter, and DispatcherHandler fit together?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The server (Netty) adapts the request into an HttpHandler. That delegates to a WebHandler chain: WebFilters run first (like security), exception handlers wrap it, and at the end DispatcherHandler dispatches to a controller. Each layer returns Mono<Void>.

open as a page

How does Spring WebFlux bootstrap onto Reactor Netty — what is the role of HttpHandler and ReactiveWebServerFactory?

level: seniorimportance: should knowfreq 45%

basics

~10 s

WebFlux compiles your routes/controllers into a single HttpHandler — a server-agnostic reactive request contract. A ReactiveWebServerFactory (NettyReactiveWebServerFactory by default) adapts that HttpHandler onto Reactor Netty and starts the server.

open as a page

Explain how a single Reactor Netty event-loop thread can handle thousands of concurrent connections — what is non-blocking I/O multiplexing?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The event loop uses the OS's readiness notification (epoll/kqueue/NIO Selector) to watch many sockets at once. It only touches a connection when data is actually ready, so one thread services many connections without ever blocking on any single one.

open as a page

WebFlux is non-blocking, yet it can run on a Servlet container. How does WebFlux run on Servlet 3.1+ containers, and what does the async/non-blocking bridge require?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Servlet 3.1 added non-blocking I/O (ReadListener/WriteListener) on top of Servlet 3.0's async requests. WebFlux uses those APIs through a special servlet adapter so it can run on Tomcat, Jetty, or Undertow without blocking a thread. Its default server, though, is Netty.

open as a page

As an architect, how do you decide between the thread-per-request (MVC) and event-loop (WebFlux) runtimes for a new service, and what operational trade-offs come with the event loop?

level: principalimportance: should knowfreq 50%

basics

~20 s

Choose event-loop (WebFlux) when you need very high concurrency with lots of slow I/O and can go non-blocking end to end. Choose thread-per-request (MVC) for blocking data stacks, simpler debugging, and normal concurrency. The event loop costs you complexity and blocking-library constraints.

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

How does the same DispatcherHandler pipeline serve both annotated @Controller and functional RouterFunction endpoints? What are the design trade-offs?

level: principalimportance: nice to knowfreq 25%

basics

~10 s

Both models plug into the same pipeline through different strategy beans: annotated methods use RequestMappingHandlerMapping/Adapter; functional routes use RouterFunctionMapping and HandlerFunctionAdapter. DispatcherHandler doesn't care which — it just picks the mapping/adapter that supports each.

open as a page

How is the Reactor Netty event-loop pool sized, and how and why would you customize LoopResources in a Spring Boot WebFlux app?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

By default Reactor Netty creates max(4, CPU cores) event-loop threads, controlled by the reactor.netty.ioWorkerCount property and LoopResources. You rarely change it — the point is to match cores. Customize via a NettyServerCustomizer that calls runOn(LoopResources.create(...)).

open as a page