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?
answer
- Servlet 3.0 = startAsync/AsyncContext; 3.1 = ReadListener/WriteListener
- ServletHttpHandlerAdapter bridges HttpHandler onto servlet non-blocking I/O
- Runtimes: Netty (default), Undertow, Tomcat, Jetty
- Not DispatcherServlet — DispatcherHandler
- Edge non-blocking only; chain must also be non-blocking
basics
~20 sServlet 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.
solid answer
~40 sWebFlux is server-agnostic: it targets an abstraction, not a specific server. Its default and most natural runtime is **Reactor Netty**, but it also runs on Tomcat, Jetty, and Undertow via the **Servlet 3.1 non-blocking I/O** APIs. Servlet 3.0 introduced async request processing (`startAsync`, `AsyncContext`), and Servlet 3.1 added truly non-blocking reads/writes via `ReadListener`/`WriteListener` on the input/output streams. Spring bridges WebFlux to these through `ServletHttpHandlerAdapter`, which wraps the reactive `HttpHandler` in a servlet and drives it with async, event-driven I/O so no container thread blocks on the socket. The requirement is a **Servlet 3.1+** container running in async, non-blocking mode — plain blocking servlet I/O won't do. Note the whole app chain must still be non-blocking to get the benefit; the servlet bridge only handles the HTTP edge.
code
java · 27 lines// Boot picks the runtime by classpath. Swap Netty for Tomcat to run WebFlux
// on the Servlet 3.1 non-blocking bridge:
//
// build.gradle
// implementation('org.springframework.boot:spring-boot-starter-webflux') {
// exclude group: 'org.springframework.boot', module: 'spring-boot-starter-reactor-netty'
// }
// implementation 'org.springframework.boot:spring-boot-starter-tomcat'
//
// Same handler code runs unchanged on Netty, Tomcat, Jetty, or Undertow:
@RestController
class StreamController {
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<Long> stream() {
return Flux.interval(Duration.ofSeconds(1)); // driven via ServletHttpHandlerAdapter on Tomcat
}
}
// Prefer a reactive WebFilter over a blocking servlet Filter on WebFlux:
@Component
class TraceWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange)
.contextWrite(ctx -> ctx.put("traceId", UUID.randomUUID().toString()));
}
}go deeper
Know that WebFlux's default server is Netty but it can also run on Tomcat/Jetty via newer servlet APIs.
Distinguish Servlet 3.0 async from 3.1 non-blocking I/O and name that WebFlux uses the latter through an adapter.
Name ServletHttpHandlerAdapter, HttpHandler, ReadListener/WriteListener, and that WebFlux uses DispatcherHandler not DispatcherServlet.
Reason about when the servlet bridge is justified (WAR/ops constraints) vs Netty, and that the edge being non-blocking is necessary but not sufficient.
## WebFlux is decoupled from the server Spring WebFlux is built on a small server abstraction: the reactive **`HttpHandler`** (and the higher-level `WebHandler`). Any server that can feed request bytes into an `HttpHandler` and stream the response back can host WebFlux. Spring ships adapters for four runtimes: - **Reactor Netty** — the *default*, natively non-blocking, event-loop based; the best fit. - **Undertow** — via its XNIO non-blocking APIs. - **Tomcat** and **Jetty** — via the **Servlet 3.1** non-blocking I/O APIs. ## The servlet evolution that made this possible Historically the Servlet API was strictly blocking: `request.getInputStream().read()` blocked the thread until bytes arrived, and one thread served one request end-to-end. Two spec steps changed that: 1. **Servlet 3.0 (async processing):** `HttpServletRequest.startAsync()` returns an `AsyncContext`, letting a servlet *release the container thread* and finish the response later from another thread. This decouples the request lifetime from the initial worker thread — but the actual reads/writes were still blocking. 2. **Servlet 3.1 (non-blocking I/O):** added `ReadListener` and `WriteListener` registered on `ServletInputStream`/`ServletOutputStream` via `setReadListener`/`setWriteListener`. The container now *calls you back* (`onDataAvailable`, `onWritePossible`) when I/O can proceed without blocking, and you check `isReady()`. This is the event-driven, non-blocking socket handling WebFlux needs. ## How Spring bridges them Spring's **`ServletHttpHandlerAdapter`** is an actual `Servlet` (registered as `HttpHandlerServlet`) that adapts the reactive `HttpHandler` onto the Servlet 3.1 async + non-blocking APIs. When a request arrives it calls `startAsync()`, wires `ReadListener`/`WriteListener` to Reactor `Publisher`s that emit `DataBuffer`s, runs your reactive pipeline, and completes the `AsyncContext` when the `Mono<Void>` finishes. So on Tomcat/Jetty, WebFlux uses the container's **non-blocking connector**, not the classic blocking one, and no container worker thread parks on the socket. Spring Boot auto-configures the right adapter based on which server is on the classpath: exclude `spring-boot-starter-reactor-netty` and add `spring-boot-starter-tomcat`, and Boot wires the servlet-based reactive runtime instead. ## Requirements and constraints - **Servlet 3.1+ container in async, non-blocking mode.** A Servlet 3.0-only container gives you async but not non-blocking reads/writes; a purely blocking setup defeats the purpose. - **Not the MVC DispatcherServlet.** WebFlux does *not* run through `DispatcherServlet`; it uses its own `DispatcherHandler`. On a servlet container it's still driven by the reactive stack via the adapter above — you don't get the MVC programming model. - **End-to-end non-blocking still required.** The servlet bridge only makes the *HTTP edge* non-blocking. If your handlers call blocking JDBC, you re-block threads downstream and lose the scalability regardless of the server. - **Netty is preferred.** Because Netty is non-blocking by design, it's the recommended and default runtime; the servlet path exists mainly for teams that must keep Tomcat/Jetty (e.g., existing ops, WAR deployment, servlet filters). ## Gotchas - **Servlet Filters:** classic blocking `javax/jakarta.servlet.Filter`s can still block the request on a servlet container; prefer `WebFilter` (WebFlux's reactive filter) instead. - **WAR vs Netty:** if you must deploy a WAR to an external servlet container, you're on the servlet bridge, not Netty. - **Don't confuse with MVC async.** Servlet 3.0 async also powers MVC's `DeferredResult`/`Callable`/`StreamingResponseBody`, but that's still the blocking MVC stack using async plumbing — not WebFlux.
- Which Spring class adapts the reactive HttpHandler onto a servlet container, and what servlet feature does it rely on?ServletHttpHandlerAdapter (registered as a servlet). It relies on Servlet 3.1's non-blocking I/O — ReadListener/WriteListener on the servlet input/output streams — plus Servlet 3.0 async (startAsync/AsyncContext).
- If WebFlux can run on Tomcat, why is Reactor Netty still the default and recommended runtime?Netty is non-blocking end-to-end by design, so it's the most efficient and natural fit. The servlet bridge exists mainly for teams tied to Tomcat/Jetty (WAR deploys, existing servlet infra), and even then the whole chain must stay non-blocking to benefit.
saying these in an interview costs you the question
- Saying WebFlux only runs on Netty and can't use Tomcat
- Claiming WebFlux uses DispatcherServlet on servlet containers
- Confusing Servlet 3.0 async (MVC DeferredResult) with WebFlux's non-blocking stack
- Thinking the servlet bridge makes blocking JDBC downstream non-blocking