← Back to list

Virtual Threads: Scaling High-IO Applications Without Reactive Complexity

Write blocking code. Scale like non-blocking.

Aesha Shingala in Simform Engineering · 2026-06-01 06:15 · 154 claps · 6.9 min read
#java #spring-boot #threads #virtual-threads #concurrency
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Virtual Threads: Scaling High-IO Applications Without Reactive Complexity

Write blocking code. Scale like non-blocking.

For a long time, Java developers had to choose between two approaches. One option was writing simple blocking code backed by thread pools. The other was adopting reactive programming to achieve better scalability, often at the cost of increased complexity.

With the release of Java 21, this long-standing compromise begins to disappear.

Virtual Threads, introduced as part of Project Loom, mark one of the biggest evolutions in Java’s concurrency model in the last twenty years. Rather than being just another performance enhancement, they redefine how thread management works in modern Java applications.

In this article, we’ll look at why virtual threads are important, understand the internal mechanics behind them, and examine their practical impact on real-world Spring Boot applications.

The Hidden Bottleneck in Modern Backend Systems

Most modern backend systems are not limited by CPU processing power — they are limited by IO operations.

A typical REST API request often involves several external interactions, such as:

  • Fetching data from a database
  • Communicating with another microservice
  • Reading from a cache
  • Publishing messages to a queue or messaging broker

In these scenarios, the application spends much more time waiting than actually performing computations.

The thread handling the request may be idle while:

  • waiting for a database query to complete,
  • waiting for a network response,
  • or waiting for file or disk operations.

In the traditional Java threading model, even an idle thread continues to occupy valuable system resources during this waiting period.

Platform Threads: Powerful but Heavy

Until Java 21, every Java thread was what we now call a platform thread. Each Java thread maps directly to an operating system thread.

1 Java thread : 1 OS thread

1 Java thread : 1 OS thread

That mapping comes with costs:

  • Each thread allocates a large stack (often around 1MB by default).
  • Creating threads is expensive.
  • Context switching between threads is expensive.
  • The operating system scheduler controls execution.

This model works well at moderate scale, but problems emerge under high concurrency.

Consider a system handling 5,000 simultaneous requests where each request waits on a 2-second database operation. In a traditional thread-per-request model, this would require around 5,000 active threads simply sitting idle during the wait time.

If each thread allocates roughly 1MB of stack memory, the application would consume nearly 5GB of memory just for thread stacks alone. Beyond memory usage, heavy thread creation also introduces significant context-switching overhead, causing the CPU to spend more time managing threads than performing actual processing work.

To avoid this, we introduced thread pools.

Thread Pools: A Controlled Compromise

Thread pools were introduced to control the number of concurrently running threads. Instead of spawning thousands of threads, applications typically maintain a fixed pool — for example, 200 worker threads — while additional requests are placed in a queue.

This approach prevents excessive memory consumption, but it creates a different challenge: request waiting time.

If 500 requests arrive simultaneously and the thread pool can handle only 200 at a time, the remaining 300 requests are forced to wait until a thread becomes available. Even when the CPU has spare capacity, users may still experience increased response times simply because no threads are free to process their requests.

As a result, the primary limitation is no longer CPU performance, but the availability of threads themselves.

For many years, this limitation was considered a normal part of backend application design.

Reactive Programming: Scalability Through Complexity

To overcome the limitations of blocking threads, many organizations turned to reactive programming frameworks like Spring WebFlux.

Reactive architectures rely on non-blocking IO and event-driven execution models rather than assigning a dedicated thread to every request. This enables applications to handle a large number of concurrent operations using a relatively small number of threads.

While this approach improves scalability, it also introduces additional complexity for developers, including:

  • A steeper learning curve
  • More difficult-to-read application flow
  • Increased debugging complexity
  • Stack traces that are harder to follow and understand

As a result, developers often had to compromise between writing simple, readable code and building highly scalable systems.

Project Loom aims to eliminate that trade-off.

Virtual Threads

Virtual threads are lightweight threads that are managed by the JVM instead of being directly controlled by the operating system.

In the traditional model, each Java thread is tightly bound to a single OS thread. Virtual threads work differently — the JVM schedules a large number of virtual threads over a much smaller set of platform threads, often referred to as carrier threads.

This distinction fundamentally changes how blocking operations behave.

With platform threads, a blocking operation also blocks the underlying OS thread, preventing it from doing any other work during that time.

With virtual threads, however, only the virtual thread is suspended while waiting. The carrier thread is immediately freed and can continue executing other virtual threads. This allows applications to handle massive numbers of concurrent tasks without requiring thousands of expensive OS-level threads.

How Virtual Threads Work Internally

When a virtual thread performs a blocking operation — for example, a database call — the JVM performs something remarkable.

Instead of letting the OS thread block, the JVM:

  1. Suspends the virtual thread.
  2. Saves its stack state into heap memory.
  3. Detaches it from the carrier thread.
  4. Assigns the carrier thread to another runnable virtual thread.

When the IO operation completes, the virtual thread is resumed and mounted again on a carrier thread. This process is often described as “mounting” and “unmounting.”

The result is that blocking code no longer blocks valuable OS resources.

Virtual threads are build over Carrier threads

Virtual threads are build over Carrier threads

Traditional platform threads allocate a fixed stack upfront. Virtual threads do not. Virtual thread stacks grow and shrink dynamically and are stored on the heap. Only active frames occupy memory. This dramatically reduces memory footprint.

Where platform threads might struggle beyond a few thousand threads, virtual threads can scale to hundreds of thousands — even millions — depending on workload. This doesn’t make your CPU faster. But it removes artificial thread limits.

Enabling Virtual Threads in Spring Boot 3.3+

One of the most exciting aspects of virtual threads is how easy they are to adopt.

In Spring Boot 3.2 and above, enabling virtual threads is as simple as adding a property:

spring.threads.virtual.enabled=true

Alternatively, one can create a bean to allow customisation:

@Bean
Executor taskExecutor() {
    return Executors.newVirtualThreadPerTaskExecutor();
}

Use the created bean to allow your code run with virtual threads:


@Autowired
Executor virtualThreadExecutor;

  public void generateReportWithVirtualThreads() {
        virtualThreadExecutor.execute(() -> {
            log.info("Virtual Thread Report - Thread: {}", Thread.currentThread());

            long startTime = System.currentTimeMillis();

            List<Employee> employees = employeeRepository.findAll();
            try {
                writeEmployeesToCsv(employees, "virtual");
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

            long endTime = System.currentTimeMillis();
            log.info("Virtual Thread Report - Time taken: {} ms, Thread: {}", (endTime - startTime), Thread.currentThread());
        });
    }

No reactive rewrite. No architectural overhaul. Minimal code change.

Benchmark: Platform vs Virtual Threads

In IO-bound scenarios, virtual threads significantly improve throughput and reduce latency under high concurrency.

Consider a scenario with:

  • 1,000 requests
  • 200 concurrent users
  • 2-second simulated IO delay

With platform threads, throughput is limited by thread pool size. Requests queue up, and latency increases.

With virtual threads, each request gets its own thread without exhausting OS resources. The system handles more concurrent work with fewer bottlenecks

To test this scenario, we can hit requests using Apache benchmark:

ab -n 1000 -c 200 http://localhost:8080/api/test

The cumulative result:

Simple java programming(no threads) vs Platform Threads vs Virtual Threads

Simple java programming(no threads) vs Platform Threads vs Virtual Threads

The performance comparison clearly shows that the /virtual endpoint delivers the best overall results, achieving the highest throughput (1940 req/sec) and the lowest response times across most metrics. While /platform also performs efficiently, /simple lags significantly in both latency and requests per second. Overall, optimised implementations dramatically improve scalability and response efficiency.

When Should You Use Virtual Threads?

Virtual threads are ideal for:

  • REST APIs
  • Microservices
  • Database-heavy applications
  • Systems that integrate with multiple external services
  • Messaging systems with blocking consumers

If your workload spends significant time waiting on IO, virtual threads are likely beneficial.

When NOT to Use Virtual Threads

Virtual threads are not a solution for CPU-bound workloads.

If your application performs heavy computation, data processing, or mathematical modeling, virtual threads won’t improve performance. CPU capacity remains the limiting factor.

They solve waiting, not computing.

Important Considerations

ThreadLocal Usage

Virtual threads create new threads frequently. Heavy reliance on ThreadLocal may impact performance.

Native Blocking Calls

If native code blocks, it may “pin” the carrier thread. This reduces scalability.

Library Compatibility

Most modern libraries are Loom-friendly. But always test under load.

Virtual Threads vs Reactive Programming

Virtual threads are not intended to replace reactive programming entirely. Instead, they provide an alternative approach for solving scalability challenges in many applications.

Reactive architectures still offer significant advantages for specific scenarios, especially in highly optimized streaming and event-driven systems. However, for a large number of standard microservices and backend applications, virtual threads introduce a much simpler way to achieve scalability.

Developers can continue writing familiar blocking-style code while still supporting a very high level of concurrency. This removes much of the complexity traditionally associated with reactive programming and makes scalable application development more accessible for many teams.

Final Thoughts

Virtual Threads in Java 21 represent one of the most important advancements in Java’s concurrency model since the introduction of executors.

They bring back simplicity without sacrificing scalability. They allow blocking code to behave efficiently under load. And for teams building IO-heavy systems in Spring Boot, they provide a powerful new default.

Virtual threads do not make Java magically faster. They make Java more scalable without making it more complicated.

Modernizing Java applications is no longer limited to framework upgrades. Features like virtual threads, structured concurrency, and cloud-native deployment models are changing how teams design scalable backend systems. Simform helps organizations evaluate and adopt modern Java and Spring architectures that improve scalability without introducing unnecessary operational complexity.


메타데이터
post_id
9d35c7fc27c4
slug
virtual-threads-scaling-high-io-applications-without-reactive-complexity-9d35c7fc27c4
url
https://medium.com/simform-engineering/virtual-threads-scaling-high-io-applications-without-reactive-complexity-9d35c7fc27c4
canonical_url
https://medium.com/simform-engineering/virtual-threads-scaling-high-io-applications-without-reactive-complexity-9d35c7fc27c4
author_url
https://medium.com/@aesha.s
status
ok
fetched_at
2026-06-14 11:28:49