Virtual threads can reduce the cost of blocking waits, but database pools, downstream capacity and JVM lifetime remain bounded resources.
Spring Boot virtual threads: cheaper waiting still needs admission control
The switch changes thread allocation
With Java 21 or later, Spring Boot can use virtual threads for supported task execution when spring.threads.virtual.enabled is true. That can make a receipt API with many blocked I/O calls use fewer platform threads. It does not make JDBC connections, memory, rate limits or the payment provider infinite. Keep the admission limits that protect those resources. Bounded execution may need to move from a platform-thread pool to a semaphore or another explicit concurrency gate once pool-size settings no longer govern the virtual-thread executor.
Inspect the blocking path
A virtual thread can be pinned to a carrier in some blocking situations, reducing throughput instead of improving it. Measure with representative traffic and inspect JVM pinning events rather than assuming the switch helps every workload. CPU-heavy PDF rendering gains little from cheap blocking, and a database pool with 47 connections still runs at most 47 concurrent queries on that pool. Shared singleton state remains shared across virtual threads; thread count is not an isolation rule.
Keep the process alive intentionally
Virtual threads are daemon threads. A scheduler-heavy process with no other non-daemon work can exit unless the application keeps the JVM alive; spring.main.keep-alive is the Boot setting for that case. Test a worker-only deployment after startup, after an idle period and during shutdown. Compare throughput, p95 latency, database wait time and carrier pinning with the old setup. Worker drain still needs a durable retry story when the process stops.
Implementation contract
spring.threads.virtual.enabled=true
spring.main.keep-alive=trueCost and verification
Virtual threads reduce per-wait thread overhead, but each task still retains stack state and application objects. A surge of 4,700 waiting requests can exhaust a connection pool or downstream quota even when thread creation stays cheap.
Common Mistakes
- Do not remove database or remote-call concurrency limits after enabling virtual threads.
- Do not assume a virtual-thread switch speeds CPU-bound receipt rendering.
- Do not ignore pinning measurements or daemon-thread process lifetime for worker-only deployments.
Read next
Spring task executors: reject work when every slot is occupied, Spring task executors: capacity, rejection and lost context, Spring singleton state: one bean does not mean one request at a time, Spring worker shutdown: release admission before a lease expires, Spring Boot graceful shutdown: finish accepted requests within a deadline.
