Java Virtual Threads vs Spring @Async – When NOT to Use Virtual Threads
Virtual Threads promise massive scalability with minimal effort.
Spring’s @Async promises controlled parallelism.
Choosing the wrong one can silently destroy performance.
If you are new to Virtual Threads, start with Java 25 Virtual Threads – Benchmarks & Pitfalls . This article focuses on decision boundaries, not syntax.
Why This Article Exists
Most articles compare Virtual Threads and @Async as if one replaces the other.
That assumption causes:
- Thread pool exhaustion
- Database saturation
- CPU starvation
- Unbounded latency under load
These failures are frequently discussed in system design interviews —but rarely explained clearly.
1. What Spring @Async Really Does
@Async executes methods on a bounded ExecutorService.
Concurrency is intentionally limited.
@Async
public void processOrder(Order order) {
heavyCpuWork(order);
}
Key characteristics:
- Explicit thread pool sizing
- Back-pressure via queue limits
- Predictable CPU usage
2. What Virtual Threads Actually Do
Virtual Threads are JVM-managed lightweight threads optimized for blocking I/O. They scale by unmounting from carrier threads during blocking calls.
Thread.startVirtualThread(() -> {
blockingHttpCall();
});
They are excellent for:
- REST controllers
- Blocking JDBC calls
- High-concurrency I/O workloads
They are not designed for CPU parallelism.
3. Virtual Threads vs @Async – Core Differences
| Aspect | Spring @Async | Virtual Threads |
|---|---|---|
| Thread limit | Bounded | Effectively unbounded |
| Best for | CPU-heavy or controlled tasks | Blocking I/O |
| Back-pressure | Yes | No (by default) |
| Failure mode | Queue rejection | System saturation |
4. When NOT to Use Virtual Threads (Critical)
4.1 CPU-Bound Workloads
CPU-heavy tasks executed on Virtual Threads still consume carrier threads. Concurrency increases, throughput does not.
Thread.startVirtualThread(() -> {
encryptLargeFile(); // BAD
});
This causes:
- Context switching overhead
- CPU cache thrashing
- Latency spikes
@Async.
4.2 Database Connection Pool Limits
Virtual Threads do not increase database capacity. They only increase request concurrency.
If you have:
- 1000 virtual threads
- 20 DB connections
You have created a waiting room, not scalability.
This mistake is common in Spring Boot performance tuning failures.
4.3 ThreadLocal and Context Propagation
ThreadLocal usage breaks assumptions with Virtual Threads.
Context may leak, disappear, or behave unpredictably.
Java 25 introduces Scoped Values to solve this.
4.4 Fire-and-Forget Background Jobs
Virtual Threads are cheap—but not free. Unbounded background execution can:
- Delay GC
- Hide failures
- Overwhelm downstream systems
Use @Async with monitoring and rejection policies instead.
5. When Virtual Threads Are the Better Choice
- Spring MVC controllers
- Blocking REST-to-REST calls
- Legacy JDBC applications
- High fan-out I/O workflows
They work best when combined with Structured Concurrency .
6. Hybrid Architecture (Best of Both Worlds)
Production systems should use both:
- Virtual Threads for request handling
- @Async for CPU-heavy and background tasks
@RestController
class OrderController {
@PostMapping("/orders")
public void create() {
// Virtual Thread handles request
asyncService.process(); // Bounded execution
}
}
7. Interview Decision Rule (Memorize This)
| Scenario | Use |
|---|---|
| Blocking I/O | Virtual Threads |
| CPU-heavy logic | @Async |
| Background jobs | @Async |
| Web request handling | Virtual Threads |
Production Readiness Checklist
- Bound CPU-heavy tasks
- Audit ThreadLocal usage
- Right-size DB pools
- Monitor carrier thread saturation
- Use JFR in load testing
Final Takeaway
Use them where waiting dominates, not where work dominates.