Diagnosing Dropwizard Database Pool Exhaustion: Latency Spikes With a Healthy Database
Dropwizard requests timing out while the database looks healthy usually means the managed JDBC pool is exhausted. Diagnose with pool metrics and a thread dump, then fix sizing, validation, or leaks.
26 Aug 2025, 04:50 UTC

The recognizable condition
Your Dropwizard service starts timing out on requests under moderate load, but the database looks fine: low CPU, fast queries when run by hand, plenty of headroom. Application CPU is also normal. Threads are not busy — they are waiting. This pattern almost always points to exhaustion of the managed JDBC connection pool that Dropwizard builds from your DataSourceFactory configuration (the Tomcat JDBC pool in current Dropwizard versions).
The useful takeaway: the pool is a fixed-size queue in front of your database. When every connection is checked out, additional request threads park until one is returned or a wait timeout fires. Fixing it is usually a sizing or connection-hygiene problem, not a database problem — but you should confirm with metrics and a thread dump before changing anything.
Symptom-to-cause table
| Symptom | Likely cause | First thing to check |
|---|---|---|
| Timeouts and rising p99 only under concurrent load | maxSize too small for concurrent queries | Pool waiting/active gauges |
| Errors after idle periods (overnight, weekends) | Stale connections dropped by DB or firewall, no validation | validationQuery / borrow-time validation settings |
| Pool never shrinks; DB shows many idle sessions | minSize set equal to maxSize | YAML config |
| Pool drains over hours, restart temporarily fixes it | Connection leak: borrow without close | Thread dump + code audit for try-with-resources |
Ordered checks
1. Look at the pool metrics
Dropwizard registers gauges for the pool in its Metrics registry, typically visible on the admin port's /metrics endpoint (default admin port 8081). Exact metric names vary by Dropwizard version and metrics reporter, so list the metrics and grep for your pool name rather than assuming a path:
curl -s http://localhost:8081/metrics | python3 -m json.tool | grep -i -A2 "pool"
Run this from any host that can reach the admin port; no special permissions beyond network access. You are looking for gauges along the lines of active, idle, and waiting counts. If waiting is above zero during a spike while active equals your configured maximum, the pool is the bottleneck. If waiting is zero but latency is still high, your problem is elsewhere — stop here and look at query latency or downstream calls instead.
2. Review the DataSourceFactory configuration
Default pool sizes are small (historically maxSize has defaulted to a low single-digit number — confirm the default for your exact Dropwizard version rather than assuming). A typical tuned block:
database:
driverClass: org.postgresql.Driver
url: jdbc:postgresql://db.internal:5432/orders
user: orders_svc
password: ${DB_PASSWORD}
maxSize: 32
minSize: 8
checkConnectionWhileIdle: true
validationQuery: "SELECT 1"
Property names and defaults differ across versions; check the DataSourceFactory source or docs for the version you deploy. A rough sizing rule: maxSize should cover the peak number of concurrent requests that each hold a connection for the duration of a query. If you have 100 request threads but queries average 10 ms, a much smaller pool suffices; if you run 50 ms queries under 200 concurrent requests, you need substantially more.
3. Capture a thread dump during an incident
On the host running the service, as the service user (or root):
jstack <pid> > /tmp/dw-dump.txt
grep -c 'getConnection\|borrowObject' /tmp/dw-dump.txt
If many dw-* request threads are parked inside the pool's borrow path (Tomcat pool classes such as ConnectionPool / BlockingQueue waits) rather than in your application code, pool contention is confirmed. If threads are blocked in your own code holding a connection while doing slow work, that is a different fix: move the work outside the borrowed-connection scope.
4. Audit for leaks
Any code path that gets a Connection (or a JDBI handle / Hibernate session) without try-with-resources or an equivalent guaranteed close will drain the pool slowly. The signature is active count climbing monotonically until requests start timing out, with a restart resetting it.
Fixes tied to findings
- Waiting threads, no leak: raise
maxSizeto match measured concurrency, keepminSizelower so idle pools shrink, and redeploy. Verify the database's ownmax_connectionscan absorb the new total across all service instances. - Stale-connection errors after idle: enable borrow-time or idle validation with a cheap
validationQuery(SELECT 1, or the driver's built-in validation where supported). This trades a round trip per borrow for never handing out a dead connection. - Confirmed leak: fix the code path. The pool's
removeAbandonedoptions can reclaim leaked connections, but treat them as a diagnostic safety net, not a fix — abandoned removal can also kill legitimate long-running queries and mask the real bug. - Slow queries holding connections: no pool size fixes a query that takes seconds. Add indexes or restructure the query, then re-measure before resizing.
Verifying the fix
Reproduce the load in staging and watch the pool gauges: the waiting count should stay at or near zero and p99 latency should flatten. Compare error rates and p99 before and after the change under the same load profile. A thread dump taken during the retest should show request threads doing work, not parked in the borrow path. Do not declare success from a quiet production window — only a load test exercises the contention you are fixing.
When to escalate
Escalate beyond pool tuning when any of these hold after right-sizing and leak fixes:
- Waiting threads persist while active count is below
maxSize— suggests connection creation is failing or slow (DNS, TLS handshake, DBmax_connectionsreached). - The database rejects new sessions — coordinate instance count ×
maxSizeagainst server limits, or put a connection proxy such as PgBouncer in front. - Errors correlate with network idle timeouts despite validation enabled — firewall or NAT state timeouts may be killing connections between validation intervals.
- You use an ORM and sessions appear to leak across requests — that is a session-management bug at the ORM layer, not a pool configuration issue.
Version caveat: property names, defaults, and metric paths in this guide reflect long-standing Dropwizard behavior but shift between releases. Confirm each against the DataSourceFactory documentation for the version you run before changing production configuration.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.