Choosing a JDBC Connection Pool for Tomcat: Built-in Pool vs HikariCP vs DBCP2
A decision guide for picking a JDBC connection pool in Tomcat: comparing the built-in Tomcat JDBC Pool, HikariCP, and DBCP2 with concrete JNDI configurations and validation steps.
11 Oct 2026, 14:39 UTC

The decision you're actually making
Every servlet that talks to a relational database through Tomcat needs a connection pool, and Tomcat gives you a real choice: the built-in Tomcat JDBC Pool, or an external pool such as HikariCP or Apache DBCP2 wired in through a JNDI resource factory. The pool you pick determines how much configuration you maintain, what diagnostics you get when connections leak, and how much throughput you can push per database connection.
The useful takeaway: if you want zero extra dependencies and solid behavior, the built-in pool is the right default. If you have measured connection-acquisition latency as a bottleneck, HikariCP is worth the extra jar. DBCP2 is mostly relevant when you inherit it.
Constraints to settle first
- Lifecycle management: the pool should be created and destroyed with the Tomcat (or webapp) lifecycle via a JNDI
Resource, not by application code. - Leak detection: you need to find code that borrows connections and never returns them.
- Statement caching: reusing prepared statements avoids repeated parse work on the database.
- Dependency footprint: adding jars to
$CATALINA_BASE/libaffects every webapp on the server.
Comparing the supported options
| Attribute | Tomcat JDBC Pool | HikariCP | Apache DBCP2 |
|---|---|---|---|
| Setup complexity | Low (ships with Tomcat) | Moderate (extra jar + factory) | Low–moderate |
| Throughput | Good | Highest of the three | Moderate |
| Leak detection | Yes (via interceptors) | Yes (leakDetectionThreshold) | Basic |
| Statement caching | Yes | Yes | Yes |
| JNDI integration | Native factory | HikariJNDIFactory | Native / default factory |
| Monitoring | JMX attributes | Rich metrics (JMX, Micrometer) | JMX attributes |
Trade-offs in practice
Tomcat JDBC Pool integrates tightly with the container: no extra jars, automatic lifecycle handling, and interceptor-based features like ResetAbandonedTimer for reclaiming leaked connections. Its raw throughput is generally lower than HikariCP's, but for most applications the database itself is the bottleneck, not the pool.
HikariCP delivers the best connection-acquisition performance and the richest metrics, which matters if you observe threads waiting on getConnection() under load. The cost is an additional dependency and a manual factory configuration, plus you must keep the jar version compatible with your Tomcat version.
Apache DBCP2 is mature and was historically Tomcat's default, but it offers lower performance than HikariCP and sees less active development. Keep it if you already run it; don't adopt it for a new deployment.
Concrete configuration: Tomcat JDBC Pool
Define the resource in $CATALINA_BASE/conf/context.xml (shared by all webapps) or in a webapp's META-INF/context.xml (scoped to that app). This requires write access to the Tomcat configuration, typically the user that owns the Tomcat installation:
<Resource name="jdbc/OrdersDS"
auth="Container"
type="javax.sql.DataSource"
factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
url="jdbc:postgresql://dbhost:5432/orders"
username="orders_app"
password="${db.password}"
maxTotal="40"
maxIdle="10"
minIdle="4"
validationQuery="SELECT 1"
testOnBorrow="true"
removeAbandoned="true"
removeAbandonedTimeout="60"
logAbandoned="true" />Notes on the risky parts: removeAbandoned="true" with logAbandoned is your leak detector — it logs a stack trace showing where the leaked connection was borrowed, but only enable it while investigating, since it adds overhead and can kill legitimately long-running operations if the timeout is too short. testOnBorrow with a lightweight validationQuery protects against stale connections after network blips, at the cost of one round trip per borrow. Replace ${db.password} with your secret-management mechanism; avoid committing plaintext passwords.
The JDBC driver jar (e.g., PostgreSQL's driver) must be visible to the pool — place it in $CATALINA_BASE/lib for a container-level resource.
If you choose HikariCP instead
Drop the HikariCP jar (and its SLF4J dependency) into $CATALINA_BASE/lib, then use its JNDI factory:
<Resource name="jdbc/OrdersDS"
auth="Container"
type="javax.sql.DataSource"
factory="com.zaxxer.hikari.HikariJNDIFactory"
jdbcUrl="jdbc:postgresql://dbhost:5432/orders"
username="orders_app"
password="${db.password}"
maximumPoolSize="40"
minimumIdle="4"
leakDetectionThreshold="30000" />Property names differ between pools (maxTotal vs maximumPoolSize), so don't copy attributes across configurations blindly. Verify the HikariCP version supports your Tomcat and Java versions before deploying; mixing pool jars between $CATALINA_BASE/lib and WEB-INF/lib can cause class-loader conflicts.
Validating the result
After restarting Tomcat, confirm the pool actually initialized rather than assuming the XML was parsed:
- Check
catalina.outfor pool startup lines. HikariCP logs something likeHikariPool-1 - Starting...; the Tomcat pool logs initialization at INFO when JNDI resources are bound. - Deploy a minimal servlet that looks up the datasource and reports pool state:
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/OrdersDS");
try (Connection c = ds.getConnection()) {
out.println("Connected: " + c.isValid(2));
}A javax.naming.NameNotFoundException here means the resource name or web.xml/@Resource reference doesn't match — the most common failure. Also confirm you haven't defined the same JDBC URL in both a container-level resource and a webapp-level one; duplicate pools can silently double your connection count and exhaust database limits.
Limitations
Attribute names and defaults above reflect Tomcat 9/10-era behavior; Tomcat 10.1 uses jakarta.* namespaces, which affects servlet code but not the pool XML. Exact throughput differences between pools depend on workload, so treat published benchmarks as directional and measure with your own query mix if performance is the deciding factor.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.