Java virtual threads: 50× more throughput, with almost no code change

Your Java server has 200 threads. Request 201 waits in line. Virtual threads in Java 21 let you run a million of them with almost no code change. Here is how they work, what they really speed up, and the two traps that bite teams in production.

Loi LeFounder, TechNova · · 7 min read
Prefer video? The 1-minute version.

Why your server stops at 200

The classic Java server model is one thread per request. Each one is a platform thread, a real operating-system thread. It reserves up to a megabyte of stack and is expensive to create. That is why Tomcat caps its pool at 200 threads by default.

Here is the part people forget: most of the time, that thread does nothing. It waits for the database, for another API, for a file. It is a waiter standing at your table while the kitchen cooks. Two hundred waiters, two hundred tables, and everyone is just standing.

A smarter waiter

A virtual thread takes the order, hands it to the kitchen and goes to serve another table. In Java terms: when a virtual thread blocks on I/O, it steps off the real thread, and that real thread runs someone else.

Under the hood, the JVM runs virtual threads on a small pool of carrier threads, roughly one per CPU core. When a virtual thread blocks, its stack is parked on the heap, often just a few kilobytes instead of a megabyte. When the data comes back, it continues on whichever carrier is free.

The code is tiny

// one thread
Thread.ofVirtual().start(() -> handle(request));

// or one virtual thread per task
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    tasks.forEach(t -> executor.submit(t));
}

In Spring Boot 3.2 or later it is one line of configuration:

spring.threads.virtual.enabled=true

A simple test: 10,000 tasks that each wait one second

Same code, roughly 50× the throughput for this kind of work.

The myth: virtual threads are not faster threads

One request is not quicker. CPU-heavy work, like image processing or encryption, gains nothing, because there is no waiting to overlap. Virtual threads give you scale, not speed: far more requests waiting at the same time on the same hardware.

Trap 1: pinning

On Java 21, if a virtual thread blocks inside a synchronized block, it gets stuck to its carrier thread. Pin all the carriers and your whole app freezes, with plenty of CPU left.

Trap 2: the bottleneck moves, it doesn’t disappear

This one is system design. You can now start a million virtual threads, but your database pool still has ten connections. Virtual threads don’t remove the bottleneck. They move it, usually straight onto your database or a partner’s API.

Why not just go reactive?

WebFlux and other reactive stacks also give you scale, but with callback chains and stack traces nobody can read. Virtual threads give you the same scale with plain, blocking code and normal debugging. For most business apps, that is the better trade.

How TechNova can help

We help teams get more out of the Java services they already run.

  • Find the real bottleneck: threads, database, or a slow downstream call, measured under realistic load.
  • Move to virtual threads safely: check for pinning, add limits around the database and partner APIs.
  • Load-test before and after, so the gain is a number, not a guess.

Book a free 30-minute call or see our services.

The short answer

Lots of waiting on I/O? Use virtual threads. CPU-heavy work? Use a normal thread pool sized to your cores. Either way, protect your database with limits.

Sources

FAQ

Which Java version do I need for virtual threads?

Java 21 or later. They were a preview in Java 19 and 20 and became final in Java 21.

Do virtual threads make my API faster?

Not per request. They let the server handle many more requests that are waiting on I/O at the same time, which raises throughput under load.

Should I put virtual threads in a thread pool?

No. Create one virtual thread per task and limit the scarce resource, such as database connections, with a pool or a semaphore.

Java service hitting a wall under load?We find where the time really goes (threads, database or downstream calls) and fix it in a tech audit.

Read next: The dual-write problem: why your order saves but the event never arrives