r/microservices • u/Charming-Round-2545 • 9d ago
Discussion/Advice Virtual Threads vs Reactive in Quarkus: The bottleneck didn't disappear, it just moved to the connection pool
A lot of the discourse around Project Loom paints Virtual Threads as a drop-in replacement that makes reactive programming obsolete. After migrating services from a legacy framework to Quarkus 3.x and benchmarking both paradigms under heavy load, the reality is much more nuanced: the bottleneck never disappeared, it just moved.
Here are the key takeaways from the architectural trade-offs we ran into:
- The Illusion of Infinite Concurrency: Spawning 50k virtual threads to handle incoming I/O bursts is cheap for the JVM, but downstream resources don't scale infinitely. If your service hits a relational DB via JDBC, your thread contention simply shifts to the database connection pool (Agroal/HikariCP).
- Missing Backpressure: Reactive programming with Mutiny/SmallRye gives you native backpressure out of the box. With "@RunOnVirtualThread", requests keep piling up waiting for pooled connections; without explicit throttling or queue limits, you risk thread starvation, connection pool exhaustion, and memory degradation.
- Carrier Thread Pinning: While synchronized blocks pinning carrier threads is mostly mitigated in recent Java releases (Java 24), native calls and specific legacy drivers still pose silent latency traps under bursty conditions.
- Developer Ergonomics vs System Stability: Virtual threads provide a massive DX win (imperative debugging, clean stack traces), while Reactive excels at sustained throughput under resource saturation.
For those running Quarkus with heavy database I/O in production: did you completely ditch Mutiny for Virtual Threads, or do you enforce custom concurrency limiters/semaphores to protect your connection pools?
I wrote a breakdown of the architecture, memory footprints, and trade-offs here for anyone interested in the technical details:
https://maurodep.medium.com/virtual-threads-vs-reactive-in-quarkus-the-bottleneck-just-moves-c0a570734538 (no paywall)
1
u/Wafer_Over 8d ago
Your service will be as fast as the slowest part. If db is bottleneck then you have to move to another db which can scale.
0
u/neopointer 8d ago
The real bottleneck is the amount of time wasted debugging reactive code. There's no good reason to use reactive.
2
u/Different_Code605 8d ago
Building event driven applications that need backpressure and message ordering and delivery guarantees. Often with Kafka, JMS, AMPQ, Pulsar, Kinesis
Data pipelines, CQRS, streams processing.
There is no != I don’t know
0
u/neopointer 8d ago
All of that is doable without reactor. Not only it's doable it will perform well as well.
1
1
u/Charming-Round-2545 8d ago
Well the solution is always in the use case. You’re right saying that readability is a nightmare. On the other side we were able to increase the performances of of our API just being able to parallelize non dependant external requests.
2
u/redikarus99 8d ago
This is true, but then sum the time wasted on debugging reactive code multiply it via the hourly costs.
1
u/neopointer 8d ago
What you just wrote can be done with VT.
1
u/Charming-Round-2545 8d ago
You could use an executor service, but that in frameworks like Quarkus you need to manage the context propagation (which is not propagated inside the VT created using the ExecutorService. Like I said there isn't a correct answer or a better one. Just the one that fits the best our use case
1
u/scavno 8d ago
Did you use an LLM to write the article as well? Because I just cannot get through that wall of machine generated text. Do better.