When to Use Virtual Threads — Best-Fit and Bad-Fit Workloads
Fast recall
Quick revision
Core answer
ForkJoinPool are for scaling computation. On Java 21+, default to virtual threads for request handlers and migrate platform thread pools selectively.Whether the candidate understands that virtual threads are targeted at I/O-bound concurrency, not CPU parallelism, and that certain patterns (synchronized + I/O, heavy ThreadLocal) undermine them
Assuming virtual threads universally replace platform threads, or that they speed up CPU-bound work. Also: putting them in a fixed pool
Name concrete examples (Tomcat + JDBC backend = yes; image scaling service = no), mention pinning audit via JFR, and recognize that existing Netty/WebFlux stacks don't need to migrate just because Loom exists
Interview-ready guide
Interview answer
Virtual threads are the right tool for I/O-bound, high-concurrency workloads — not a universal replacement for platform threads.
Perfect fits
- REST APIs with JDBC and downstream HTTP calls
- Message consumers (Kafka, SQS) processing each message
- Fan-out outbound calls (1000 parallel HTTP requests)
- Thread-per-request web servers (Tomcat/Jetty with
spring.threads.virtual.enabled=true) - Any code currently bottlenecked by thread pool size rather than CPU
Bad fits
- Pure CPU work: image processing, parsing, encryption. Virtual threads don't add CPU parallelism — use a fixed pool sized to core count.
- Code with
synchronizedblocks wrapping blocking I/O. Pinning holds the carrier for the entire I/O; a few concurrent instances can starve all carriers. Migrate toReentrantLockfirst. - Code with large ThreadLocal state. Per-thread copies multiply across millions of virtual threads and blow up heap. Use
ScopedValuefor immutable per-scope data. - Working Netty/WebFlux apps. The migration cost exceeds the benefit — they already scale.
Mental model
- Concurrency vs parallelism: virtual threads add concurrency (many tasks overlapping), not parallelism (many tasks computing simultaneously).
- Parallelism still = carrier count = CPU count.
- Concurrency = how many virtual threads fit in heap (millions on typical hardware).
The migration audit checklist
- 1Find
synchronizedmethods/blocks that perform blocking I/O — fix by converting toReentrantLock. Use-XX:+RecordJvmLoomPinningvia JFR to detect at runtime. - 2Inventory
ThreadLocalusage — remove per-thread caching anti-patterns, migrate toScopedValuefor config. - 3Add downstream flow control: your old 200-thread pool implicitly limited DB concurrency. Now a million requests can hammer a 20-connection HikariCP pool. Add
Semaphoreor Resilience4jBulkhead. - 4Size connection pools appropriately — they're your new bottleneck.
Hybrid is common
- Virtual threads for request dispatch and I/O orchestration
- Dedicated
ForkJoinPoolsized to cores for CPU-heavy slices within a request - Offload with
CompletableFuture.supplyAsync(cpuTask, cpuPool).join()on the virtual thread
The thread-pool-sizing calculation disappears
- No more "cores × (1 + wait/compute)" math
- No more choosing between 50, 200, or 500 threads
- Just
newVirtualThreadPerTaskExecutor() - Sizing effort moves to connection pools and Semaphores
Recommendation: For any new Java 21+ server-side project, default to virtual threads via Executors.newVirtualThreadPerTaskExecutor() or Spring Boot's spring.threads.virtual.enabled=true. For existing platform-thread codebases, migrate opportunistically — audit for synchronized+I/O and ThreadLocal footprint first, and add explicit downstream flow control via Semaphore before flipping the switch. For CPU-bound work, keep using traditional thread pools — virtual threads don't change physics. Virtual threads are not magic; they're a focused tool that eliminates the I/O-scaling pain of traditional thread-per-request, letting you write simple, readable code instead of reactive chains for the 80% case.
When to Use Virtual Threads — Best-Fit and Bad-Fit Workloads
Direct Answer
Use virtual threads when your workload is I/O-bound and high-concurrency — HTTP servers, REST APIs with DB/downstream calls, message consumers, fan-out client calls. They eliminate the thread-pool sizing dance and let you write simple blocking code that scales to millions of concurrent requests. Do NOT use virtual threads for CPU-bound work — image processing, heavy parsing, math. They don't add CPU parallelism; you're still capped by core count. Also avoid them when your code holds synchronized locks across blocking I/O (pinning starves carriers) or when you rely on heavy ThreadLocal state that would multiply across millions of threads. The mental model: virtual threads are for scaling blocking I/O; platform threads / ForkJoinPool are for scaling computation. On Java 21+, default to virtual threads for request handlers and migrate platform thread pools selectively.
Common Mistakes & Pitfalls
Assuming virtual threads universally replace platform threads, or that they speed up CPU-bound work. Also: putting them in a fixed pool
Check your understanding
Answer these without looking back at the article.
- Why don't virtual threads help CPU-bound code?
- How would you detect pinning in production?
- What replaces ThreadLocal for immutable per-scope values?
- If virtual threads are unlimited, how do you protect a DB with only 20 connections?
- When would you keep a mature WebFlux service instead of migrating to Loom?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.