Skip to content
Back EndJava

High-Concurrency APIs with Java 25 Virtual Threads & Spring Boot 4

7 min readsalahtobok
Abstract visualization of Java Virtual Threads

The Concurrency Problem in Modern Backend Engineering

For years, backend engineers building high-concurrency applications faced a difficult choice. The standard Java thread-per-request model—where every incoming HTTP request consumes an entire OS thread—is incredibly easy to write and debug. However, it scales poorly. OS threads (Platform Threads) are heavy; they require around 1MB of stack space and take milliseconds to spawn.

When your Spring Boot application makes a blocking I/O call, such as querying a PostgreSQL database or communicating with a third-party REST API, the expensive OS thread sits idle, doing absolutely nothing while waiting for the response. To solve this, the industry aggressively pivoted to Reactive Programming (like Spring WebFlux and Project Reactor), introducing non-blocking event loops.

Reactive programming vs Virtual Threads complexity
Reactive WebFlux Event Loop vs. the simplicity of Virtual Threads.

While Reactive programming successfully solved the scaling issue, it introduced massive cognitive overhead. Stack traces became unreadable, debugging became a nightmare, and the entire ecosystem of blocking libraries had to be rewritten. Java 25 Virtual Threads finally offer the ultimate solution: the performance of Reactive programming combined with the simplicity of the synchronous thread-per-request model.

What Are Virtual Threads?

Introduced in Java 21 and heavily refined up through Java 25, Virtual Threads are lightweight threads managed entirely by the Java Virtual Machine (JVM) rather than the operating system. Instead of maintaining a 1:1 relationship with an OS thread, millions of Virtual Threads can be multiplexed over a small pool of carrier OS threads.

Comparison of Platform Threads vs Virtual Threads
Platform Threads are heavy and limited by the OS, while Virtual Threads are lightweight and bound only by heap memory.

Because they are allocated on the Java heap, creating a Virtual Thread takes only a few bytes of memory and microseconds of time. You no longer need thread pools to limit resource exhaustion; you can simply create a new Virtual Thread for every single concurrent task.

How Non-Blocking I/O Works Under the Hood

The true magic of Virtual Threads happens during blocking operations. When a Virtual Thread encounters a blocking I/O call (like executing a Hibernate JPA query), the JVM automatically unmounts the Virtual Thread from its underlying carrier OS thread.

Virtual Thread unmounting during blocking I/O
A Virtual Thread is parked in heap memory during I/O, instantly freeing the Carrier OS thread for another task.

The carrier thread is instantly freed up to execute a different Virtual Thread. Once the database responds, the original Virtual Thread is scheduled back onto an available carrier thread to resume execution exactly where it left off. From the developer’s perspective, the code looks entirely synchronous and blocking.

Enabling Virtual Threads in Spring Boot 4

Spring Boot 4.0.0 provides native, out-of-the-box support for Virtual Threads. There are no new frameworks to learn, and you do not need to migrate to WebFlux. You simply instruct Spring Boot to use Virtual Threads for its embedded web server (Tomcat or Undertow) and asynchronous task executors.

Add the following line to your application.properties:

spring.threads.virtual.enabled=true

With this single configuration property, Spring Boot will automatically replace the standard Tomcat thread pool with a Virtual Thread executor. Every incoming HTTP request will be handled by a brand-new Virtual Thread.

Implementing a High-Concurrency REST API

Let’s look at a standard REST Controller that simulates a slow backend service, such as aggregating data from multiple external APIs.

package com.webcodein.virtualthreads.api;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;

@RestController
@RequestMapping("/api/reports")
public class ReportController {

    private static final Logger log = LoggerFactory.getLogger(ReportController.class);

    @GetMapping("/generate")
    public String generateReport() throws InterruptedException {
        log.info("Starting report generation on thread: {}", Thread.currentThread());
        
        // Simulating a slow, blocking database query or external API call
        Thread.sleep(Duration.ofSeconds(2));
        
        log.info("Finished report generation on thread: {}", Thread.currentThread());
        return "Report generated successfully!";
    }
}

If you hit this endpoint concurrently 10,000 times using a standard Platform Thread model, Tomcat would quickly exhaust its default maximum of 200 threads, causing massive latency and connection timeouts. However, with Virtual Threads enabled, the JVM easily spins up 10,000 Virtual Threads, processes them concurrently on just a few carrier threads, and returns all responses in roughly 2 seconds without breaking a sweat.

Real-World Use Cases

  • High-Throughput Microservices: APIs that aggregate data from multiple downstream services over HTTP can now execute requests concurrently in standard synchronous loops without reactor chains.
  • Heavy Database Operations: Applications relying on complex Hibernate 7 and Spring Data JPA 4 queries can easily handle thousands of concurrent transactions since the thread footprint is virtually zero during database wait times.
  • Replacing WebFlux: Enterprise teams struggling with the maintainability of Reactive codebases can safely migrate back to standard Spring MVC without sacrificing scalability.
Exponential scaling with minimal memory using Virtual Threads
Virtual Threads allow linear scaling of concurrent requests with negligible impact on heap memory.

Advantages and Limitations

Advantages

  • Simplified Codebase: Write straightforward, sequential code. Try/catch blocks and standard control flows work perfectly.
  • Excellent Observability: Stack traces show the exact line of execution, unlike the deeply nested assembly traces of Reactive frameworks. You can easily trace requests using OpenTelemetry and Grafana in Spring Boot 4.
  • Massive Scalability: Supports millions of concurrent connections on standard hardware.

Limitations & Pitfalls

  • CPU-Bound Tasks: Virtual Threads are designed to solve I/O bottlenecks. They do not speed up CPU-bound tasks like image processing or cryptographic hashing. If a Virtual Thread executes a heavy CPU task, it will monopolize the carrier OS thread.
  • Thread Pinning: If a Virtual Thread performs a blocking operation inside a synchronized block or method, it will “pin” the carrier thread, preventing the JVM from unmounting it. This can quickly exhaust the carrier thread pool. (Note: Java 21+ heavily mitigates this, but you should prefer ReentrantLock over synchronized).
  • ThreadLocal Abuse: Because you can have millions of Virtual Threads, storing large objects in ThreadLocal variables can quickly lead to OutOfMemory (OOM) errors.

For a deeper dive into securing your production environment against these issues, read our complete guide on Java 25 Virtual Threads Production Pitfalls.

Conclusion

Java 25 Virtual Threads represent a paradigm shift in how we build high-concurrency backend systems. By upgrading to Spring Boot 4.0.0, your applications can instantly benefit from massive scalability without the steep learning curve of Reactive programming. The era of the thread-per-request model is back, stronger and more efficient than ever.

Companion Code

You can find the complete, runnable Spring Boot 4 project for this article, complete with Docker and Testcontainers support, in our official GitHub repository:
WebCodein Workshop – Virtual Threads Companion Code

Frequently Asked Questions

Do Virtual Threads make Java faster?

Virtual Threads do not make individual requests execute faster, nor do they speed up CPU-bound tasks. They improve throughput by allowing the server to handle vastly more concurrent I/O-bound requests simultaneously without exhausting system memory.

Should I stop using Spring WebFlux?

For the vast majority of standard web applications and microservices, yes. Spring MVC with Virtual Threads provides equivalent scaling for I/O-bound workloads while remaining significantly easier to write, test, and debug. WebFlux remains useful for highly specialized streaming use cases.

How do I test Virtual Threads locally?

You can simulate high concurrency locally using load-testing tools like Apache JMeter, Gatling, or specialized Java testing frameworks. Be sure to monitor memory usage using JDK Flight Recorder to ensure you aren’t falling victim to Thread Pinning.


References:

Comments