Virtual Threads Do Not Increase Database Capacity: The JDBC Pool Boundary
Why Java virtual-thread concurrency and concurrent JDBC capacity are different quantities, with HikariCP, backpressure, and queueing latency as the operational boundary.
Virtual threads make blocked I/O-heavy Java workloads much cheaper to express, but application concurrency is not the same as downstream capacity. A database connection remains a constrained resource with costs in sessions, memory, cursors, locks, I/O, and CPU.
The useful distinction is:
many virtual threads
!=
the same number of JDBC connections
!=
the same number of efficient concurrent SQL operationsThe pool is an admission-control boundary
A request can wait cheaply on a virtual thread. Giving every request its own JDBC connection is different: it expands the amount of work admitted into the database at the same time. A pool such as HikariCP therefore acts not only as a reuse mechanism but also as an admission-control boundary.
When the pool is full, waiting virtual threads can park without consuming a platform thread. That is a reason to keep waiting inexpensive, not a reason to enlarge the scarce resource without measurement.
Pool size does not follow thread count
There is no rule such as maximumPoolSize = virtualThreadCount. Sustainable pool capacity depends on database service time, SQL mix, transaction duration, I/O behavior, and latency objectives.
An oversized pool can manifest as:
- more active database sessions,
- longer CPU run queues,
- storage or network queues,
- lock contention,
- cache pressure,
- acceptable averages but degraded p95/p99 latency.
Throughput alone is therefore not enough. Connection acquisition time, active/idle/pending connections, transaction duration, database wait events, and tail latency should be observed together.
Place backpressure before the scarce resource
Virtual threads do not imply unbounded admission. If the database is the scarce resource, backpressure should be visible before it. The JDBC pool naturally creates such a waiting point, but the queue itself should not be allowed to grow without policy. Timeouts, bounded admission, or upstream rate limits are needed so overload can be rejected instead of converted into unbounded latency.
This also explains why increasing a pool can hurt rather than help. More queries may enter the database at once, but once the database is saturated each query can take longer. Queueing and tail latency then grow without a proportional increase in completed work.
JVM-version boundary
Virtual threads became a permanent Java feature in JDK 21. Later JVM work, including improvements around pinning associated with synchronized, changed application-level scheduling costs. Those changes do not remove the database's physical capacity boundary.
Measure instead of deriving a magic number
There is no deployment-independent optimal pool size. Short OLTP work and long analytical work have different service-time distributions and may justify separate admission policies or even separate pools.
Related material: High-Performance Java Data Systems, Connection Pool, Virtual Thread, Backpressure, Tail Latency.
References
- OpenJDK JEP 444 — Virtual Threads
- OpenJDK JEP 491 — Synchronize Virtual Threads without Pinning
- HikariCP Wiki — About Pool Sizing