skip to content

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%

answer

  1. default worker count = max(4, cores)
  2. reactor.netty.ioWorkerCount system property
  3. LoopResources = the EventLoopGroup abstraction
  4. runOn(LoopResources.create(...)) via NettyServerCustomizer
  5. more loops != more throughput; fix blocking instead

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(...)).

solid answer

~40 s

Reactor Netty sizes its worker event loops via LoopResources, defaulting to max(4, Runtime.availableProcessors()) — one loop per core so all cores stay busy. It is overridable globally with the system property reactor.netty.ioWorkerCount. You should almost never raise it: event loops are for non-blocking I/O, so more threads than cores just add context switching without adding throughput; the fix for a saturated loop is removing blocking work, not adding loops. Legitimate reasons to customize LoopResources include running the server and an outbound WebClient on separate, named loop groups for isolation and clearer diagnostics, tuning selector vs worker thread counts, or preferring native epoll/kqueue transport. In Spring Boot you customize by registering a WebServerFactoryCustomizer<NettyReactiveWebServerFactory> that adds a NettyServerCustomizer calling httpServer.runOn(LoopResources.create("name", selectCount, workerCount, daemon)).

code

java · 23 lines
java
@Configuration
class NettyLoopConfig {

    // Give the inbound server its own named event-loop group so
    // thread dumps clearly separate server loops from WebClient loops.
    @Bean
    WebServerFactoryCustomizer<NettyReactiveWebServerFactory> serverLoops() {
        LoopResources serverLoop =
                LoopResources.create("katajob-http", 1, 8, true); // select=1, worker=8, daemon
        return factory -> factory.addServerCustomizers(
                httpServer -> httpServer.runOn(serverLoop));
    }

    // Isolated loops for outbound calls (optional).
    @Bean
    WebClient webClient() {
        LoopResources clientLoop = LoopResources.create("katajob-wc", 1, 4, true);
        HttpClient http = HttpClient.create().runOn(clientLoop);
        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(http))
                .build();
    }
}

go deeper

for a junior

Not expected — just knows the pool is small.

for a middle

Knows the default is roughly one loop per core and that you don't fix blocking by adding loops.

for a senior

Can state the default formula and the ioWorkerCount property and explain why over-sizing doesn't help.

for a principal

Chooses LoopResources customization deliberately for isolation/transport/container-correctness, wires it via NettyServerCustomizer, and validates with load tests and thread dumps.

## What `LoopResources` is `reactor.netty.resources.LoopResources` is Reactor Netty's abstraction over the Netty `EventLoopGroup`(s) — the pool(s) of event-loop threads (`reactor-http-nio-*`). It decides how many **selector** threads (accept connections) and **worker** threads (handle I/O) exist and whether they use the JDK NIO or native (epoll/kqueue) transport. ## Default sizing The worker count defaults to: ``` max(4, Runtime.getRuntime().availableProcessors()) ``` resolved by `reactor.netty.ReactorNetty` from the system property **`reactor.netty.ioWorkerCount`** (fallback to CPU count, floored at 4). By default the **server** and outbound clients (`WebClient`/`HttpClient`) **share** a global `LoopResources` (`loopResources` / `colocate` semantics), so a WebClient call can even run on the same loop that received the request — reducing thread hops. ## Why one loop per core Event loops do **non-blocking** work, so a loop is CPU-bound while active. Beyond one busy loop per core, extra loops cannot run in parallel — they just add context-switching and cache churn. Therefore: - **Do not** 'scale up' `ioWorkerCount` to fix latency. A saturated loop means either genuine CPU saturation (add cores/instances) or, far more commonly, **blocking work on the loop** (fix by offloading to `Schedulers.boundedElastic()` or using reactive drivers). - Lowering it is occasionally useful in constrained/containerized environments where `availableProcessors()` misreports. ## Legitimate reasons to customize 1. **Isolation / diagnostics** — give the server and a heavily used `WebClient` **separate, named** `LoopResources` so you can tell in a thread dump which loops serve inbound vs outbound, and so a storm on one does not perturb the other. 2. **Transport selection** — force or verify native **epoll**/**kqueue** (add `netty-transport-native-epoll`); `LoopResources.create(name, ..., preferNative=true)`. 3. **Selector vs worker split** — tune the number of acceptor selectors separately from workers for extreme connection-accept rates. 4. **Container CPU limits** — pin an explicit worker count when the JVM sees the wrong core count. ## How to customize in Spring Boot Register a `WebServerFactoryCustomizer<NettyReactiveWebServerFactory>` that adds a `NettyServerCustomizer`: ```java @Bean WebServerFactoryCustomizer<NettyReactiveWebServerFactory> loopCustomizer() { return factory -> factory.addServerCustomizers(httpServer -> httpServer.runOn(LoopResources.create( "katajob-http", /*select*/ 1, /*worker*/ 8, /*daemon*/ true))); } ``` `runOn(LoopResources)` on the Reactor Netty `HttpServer` swaps in your group. For a `WebClient`, configure its `HttpClient` with `.runOn(separateLoopResources)`. ### Property-only knobs Many common tunables need no code: `server.port`, `server.netty.connection-timeout`, `server.netty.idle-timeout`, `server.netty.max-keep-alive-requests`, and the JVM flag `-Dreactor.netty.ioWorkerCount=N`. ## Gotchas - **Global vs per-server**: setting `-Dreactor.netty.ioWorkerCount` changes the shared default for *both* server and clients; `runOn` scopes it to one server/client. - **Daemon threads**: create loops as daemon (or manage disposal) so they don't block JVM shutdown. - **Colocation**: by default WebClient may reuse the request's event loop; if you deliberately isolate loops you lose that optimization — measure before/after. - **Blocking is still the real bug**: no `LoopResources` tuning rescues an app that blocks its loops. Fix the blocking first. - **`availableProcessors()` in containers**: older JVMs / misconfigured cgroups can report host cores; verify and pin if needed. ## When to touch it Almost never for throughput. Reach for `LoopResources` customization for **observability/isolation**, **transport choice**, or **container core-count correctness** — and always confirm with load tests and thread dumps rather than guessing.

  • Your reactor-http-nio threads are saturated under load. Do you raise ioWorkerCount? Why or why not?
    No, not first. Event loops are non-blocking and CPU-bound; more loops than cores just add context switching. Saturation almost always means blocking work on the loop (offload to boundedElastic or use reactive drivers) or true CPU exhaustion (scale out / add cores). Only after ruling those out, and never above the core count for pure I/O, would sizing enter the picture.
  • Why might you give the server and WebClient separate LoopResources?
    For isolation and observability: a burst of outbound calls can't starve inbound handling, and named groups make thread dumps and metrics attributable to inbound vs outbound. The trade-off is losing the default colocation optimization where WebClient reuses the request's event loop, so you should measure the impact.

saying these in an interview costs you the question

  • Recommending large ioWorkerCount values to boost throughput
  • Treating LoopResources tuning as the fix for a blocking app
  • Thinking event-loop threads scale like a Tomcat request pool
  • Ignoring container CPU-count misreporting when sizing loops
  • Creating non-daemon loop groups that hang JVM shutdown

context