Enterprise Java

Virtual Threads Are Quietly Killing WebFlux. Is That a Good Thing?

Teams are flipping one property, spring.threads.virtual.enabled=true, ripping out reactive pipelines, and getting the same or better tail latency back. That’s not an accident. It’s the JVM finally fixing the problem reactive programming was built to work around.

By mid-2026, this isn’t a hot take anymore, it’s a documented pattern. Teams that spent years building Mono and Flux chains are quietly rewriting them as plain, blocking, thread-per-request code, flipping spring.threads.virtual.enabled=true, and reporting comparable or better P99 latency for the trouble. If that sounds backward, it’s only backward if you assume reactive programming was ever the actual goal. It wasn’t. It was a workaround for a specific, expensive problem: the JVM’s old threads cost too much to hold open while waiting on I/O. Virtual threads solve that problem directly, at the runtime level, which raises an uncomfortable question for anyone still reaching for WebFlux by default: what, exactly, is it buying you now?

What Reactive Programming Was Actually Solving

It’s worth being precise about the original problem, because the answer explains everything that follows. A traditional Java platform thread maps one-to-one onto an OS thread, and each of those costs real memory, typically a megabyte or more of stack space, plus the overhead of an OS-level context switch every time the scheduler moves between them. Under Spring MVC’s classic thread-per-request model, a slow database call or a slow downstream HTTP request meant that thread sat there, fully allocated, doing nothing, for the entire duration of the wait. Scale that to thousands of concurrent slow requests and you run out of threads long before you run out of CPU.

WebFlux, built on Project Reactor and Netty’s non-blocking event loop, solved that specific problem by refusing to let any thread sit idle on I/O at all. A small, fixed pool of event-loop threads handles enormous concurrency by never blocking, ever, which meant every blocking call in your dependency chain, JDBC drivers included, had to be replaced with a non-blocking, callback-based or operator-based equivalent. That’s a real, working solution. It’s also a solution that pushes its cost onto everyone who writes code inside it: callback chains, operator fusion, debugging stack traces that no longer resemble your actual call path, and an entire ecosystem of libraries that need a reactive-specific rewrite before they’re usable.

The Provocation: Reactive Was Never the Point

Here’s the case worth sitting with directly: reactive programming’s real contribution was never “non-blocking code is better.” It was “don’t let a thread sit idle while it’s blocked,” and that idea only required rewriting your application into callbacks and operators because the JVM’s threads happened to be expensive. Virtual threads, delivered in Java 21 via JEP 444, attack that cost at its actual source instead. A virtual thread is scheduled by the JVM rather than the OS, costs a small, resizable stack rather than a fixed multi-megabyte allocation, and, critically, unmounts itself from its carrier platform thread the moment it blocks on I/O, freeing that carrier to run other virtual threads in the meantime.

That’s the same underlying fix reactive programming was providing, cheap idle waiting, delivered by the runtime instead of by your code. Write ordinary, blocking, thread-per-request Java, the kind every Java developer already knows how to debug, profile, and reason about, and the JVM now gives you the scalability WebFlux used to be the only way to get. If that’s true, and the benchmarks below suggest it largely is, then reactive programming wasn’t a superior programming model that happened to also solve a performance problem. It was a performance workaround that happened to require a different programming model, and now that the JVM has closed the actual gap, the workaround has lost its reason to exist for a large share of the workloads that adopted it.

The Data Behind the Shift

This isn’t just architectural argument. The most detailed public benchmark on this exact question, run on bare metal with Amazon Corretto 25 in June 2026, pit Spring Boot with virtual threads directly against WebFlux across a range of realistic workloads combining database queries and downstream REST calls.

Median Latency at 60,000 Concurrent Connections

Both variants ran on Netty under the heaviest fixed-rate test scenario in the benchmark.Source: Chris Gleissner, loom-webflux-benchmarks (June 2026 run)

At the heaviest load tested, virtual threads held a flat median latency around 100 milliseconds. WebFlux degraded sharply at the same load, with median latency climbing past a full second and P99 reaching roughly 3.3 seconds, accompanied by heap usage near capacity and well over a minute and a half of cumulative garbage collection time. That’s not a small gap, and it’s not the direction most engineers would have predicted a few years ago.

Head-to-Head Win Rate, Virtual Threads vs. WebFlux (Both on Netty)

Share of individual per-metric, per-scenario contests won by each approach across the full benchmark suite’s default scenarios.Source: Chris Gleissner, loom-webflux-benchmarks (June 2026 run)

Virtual threads won roughly half of all individual contests, WebFlux won about a quarter, and the rest showed no statistically meaningful difference either way. That’s not “virtual threads always win.” It’s “virtual threads are now competitive or better in the majority of realistic cases,” which is a genuinely different claim than reactive advocates were making as recently as 2023, and it’s the claim actually driving the migrations happening right now.

The Pinning Objection Just Aged Out

Anyone who tried virtual threads early has a specific objection loaded and ready: synchronized blocks pin a virtual thread to its carrier, defeating the entire point, and Java’s own early guidance was to rip out synchronized in favor of ReentrantLock everywhere before trusting virtual threads at scale. That objection is now out of date. JEP 491, delivered in Java 24, rewrote the JVM’s monitor implementation specifically so that a virtual thread blocking inside a synchronized method or statement unmounts from its carrier just like it would for any other blocking call, eliminating nearly all pinning caused by synchronized without requiring a single line of application code to change. The remaining pinning cases are narrow: native code called through JNI or the Foreign Function & Memory API that blocks back inside Java. For the overwhelming majority of ordinary Spring Boot applications, upgrading the JDK now fixes what used to require a manual audit of every synchronized block in your dependency tree.

That detail matters more than it looks. It means the case for virtual threads isn’t “mostly solved, with an asterisk you need to manage.” It’s “solved architecturally, at the runtime level,” which is precisely the framing that makes reactive programming’s remaining justification harder to defend by default.

Where WebFlux Still Actually Wins

None of this makes WebFlux wrong to use, and it’s worth being direct about where it still earns its complexity. Reactive Streams’ backpressure model, the mechanism that lets a slow consumer signal a fast producer to slow down, has no equivalent in a thread-per-request world; virtual threads make blocking cheap, but they don’t give you flow control over an unbounded stream. High fan-out gateway services that aggregate dozens of downstream calls per request, and genuinely streaming workloads like server-sent events or large result sets processed incrementally, are still comfortably WebFlux’s territory. And the benchmark evidence isn’t uniformly one-sided either: at least one published comparison, testing JWT verification against a MySQL-backed service, found WebFlux outperforming a virtual-threads equivalent by a wide margin, a reminder that workload shape still matters and a single benchmark, including the one driving most of this article, is not a universal verdict.

Side by Side

FactorVirtual Threads (Tomcat/Jetty)WebFlux (Netty)
Code styleOrdinary blocking, thread-per-request JavaNon-blocking, operator chains (Mono/Flux)
DebuggabilityStandard stack traces, familiar profilersStack traces span reactive operators, harder to read
Backpressure / flow controlNot availableNative to the Reactive Streams model
Library compatibilityAny blocking library works unmodifiedRequires non-blocking drivers (R2DBC, WebClient, etc.)
synchronized block pinningResolved at the JVM level since JDK 24 (JEP 491)Not applicable; no thread-per-request model to pin
Best fitStandard CRUD APIs, DB-backed services, most microservicesStreaming endpoints, high fan-out gateways, backpressure-sensitive pipelines

Quick Checklist: Should This Service Still Be WebFlux?

  • Does this endpoint need real backpressure, a slow consumer actively signaling a fast producer? If not, that’s one of WebFlux’s core advantages you’re not using.
  • Is this a genuinely streaming workload (server-sent events, large incremental result sets) rather than a request/response API wrapped in reactive syntax?
  • Are you on Java 24 or later? If not, upgrade before drawing any conclusions about virtual-thread pinning; JEP 491 changes the calculus substantially.
  • Would a plain blocking library “just work” here if you swapped it in? If yes, you’re likely paying reactive’s complexity tax for a benefit virtual threads now provide for free.
  • Benchmark your actual workload shape before migrating either direction; the published data shows virtual threads winning most of the time, not all of the time.

What We Have Learned

Reactive programming’s value proposition was never really about a better programming model, it was about not letting a thread sit idle on I/O, and that constraint only forced a rewrite into callbacks and operators because platform threads used to be too expensive to hold open. Virtual threads remove that constraint directly, at the JVM level, letting ordinary blocking code get the scalability that used to require WebFlux, and the most detailed public benchmark available backs that up: virtual threads won roughly half of all head-to-head contests against WebFlux on the same Netty runtime, and held flat latency at loads where WebFlux degraded sharply.

The pinning objection that used to complicate this story is largely resolved as of JDK 24’s JEP 491. None of this retires WebFlux entirely, backpressure and genuine streaming workloads are still its comfortable home, and workload shape can still flip the result the other way, as at least one published benchmark shows. What it does retire is the habit of reaching for WebFlux by default. The question worth asking on your next new service isn’t “should we go reactive,” it’s “does this specific endpoint need something virtual threads don’t already give us for free.”

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button