DataSource and HikariCP: Pool Internals and Tuning

bee2026-10-0842 min read0 views
Why not open a connection per request? Why is HikariCP so fast? And how big should the pool be — pool internals, a parameter table and monitoring, turned into a tunable craft.
1 / 140
Section
0. The 30-second version
2 / 140

A database connection is a phone line: dialling it goes through the exchange (the TCP handshake), you state your name and password (authentication), then you agree on language and time zone (session initialisation) — 5–20 ms in total. But the sentence you actually want to say (one indexed SQL statement) usually takes under 1 ms. In other words, if every request dials from scratch, nine tenths of your bill is for "getting through", not for "talking". A connection pool does exactly one thing: lay out a batch of already-dialled lines, let anyone borrow one, and hang up when done — and note that "hanging up" does not cut the line, it only marks it idle.

3 / 140

Six terms, one line each (used throughout):

4 / 140
  • Connection: an established channel between your app and the database; underneath it sits a TCP socket, which is expensive to build — hence precious
  • DataSource: a JDBC-spec interface with a single contract, getConnection() — "give me a usable connection". Whether it is brand new or borrowed is none of the caller's business
  • Connection pool: the rental station that implements DataSource; it pre-creates, reuses, health-checks and recycles connections
  • HikariCP: the pool Spring Boot ships by default (the name means "light" in Japanese); fast because of lock-free borrowing, proxy wrapping and background housekeeping
  • Borrow / return: taking a connection out of the pool is borrowing; calling close() afterwards is returning it. Inside a pool close() does not mean disconnect
  • Leak: borrowed but never returned — one thread keeps gripping a connection while everyone else queues, and the pool slowly drains dry
5 / 140
类比|Analogy

a connection pool is the bicycle-share dock at your apartment gate. Riding costs almost nothing; what costs is "manufacturing a bike and trucking it here" (TCP + authentication). So somebody decided to park a batch of bikes up front — scan, ride, dock it again. The correspondences are worth memorising: the number of docking slots = maximumPoolSize; every bike ridden so you can only stand there = requests queueing in the pool (pending); you scan, wait thirty seconds, give up and walk = connectionTimeout firing; riding a bike home and never docking it = a connection leak, and tomorrow the whole estate has no bikes; the overnight crew hauling away rusted frames = HikariCP's HouseKeeper evicting connections past maxLifetime / idleTimeout. And the counter-intuitive part: more slots does not mean faster riding — the road (your database) is only so wide, and too many bikes just creates a jam.

6 / 140
Diagram
Figure · New connection per request versus pooling
Figure · New connection per request versus pooling
7 / 140

The left side is the real bill for "dial a fresh connection every time"; the right side is what pooling gives you. Everything else in this article answers two questions: why the right side is mandatory, and where exactly to set its knob.

8 / 140

After this article you should be able to answer three questions:

9 / 140
  • Why does Spring Boot reach the database without a single line of pool configuration? Who is that "default pool"?
  • Is maximumPoolSize better when bigger? If not, why does the official rule of thumb come out as something as small as "cores × 2 + spindles"?
  • Production suddenly floods with Connection is not available, request timed out after 30004ms — which metric do you look at first, and which config line?
10 / 140

Turn the knob first and get a feel for it — this is the article's only sandbox; change one parameter and the three numbers on the right move immediately:

11 / 140
Sandbox
SandboxWhere should maximumPoolSize sit
Result
pending: 0
P99 acquire time: 2ms
DB Threads_connected: 10/200
hikaricp.connections.usage ≈ 6.8ms per use
Just right: nobody queues and the database is nowhere near saturated. This is where Boot's default of 10 sits for most small and mid systems — not arbitrary, but the "enough without breaking the DB" band
12 / 140
Note

the one constraint worth burning in from that sandbox is pool size × app instance count < database max_connections. The database's connection ceiling is the global bottleneck; pools merely divide it into slices.

13 / 140
Section
1. The cost of one TCP handshake plus authentication
14 / 140

"Why not open a new database connection on every request?" Start with the bill. Establishing a genuinely usable connection means doing all of this, in order:

15 / 140
Table
StageRough costWhat happens
TCP three-way handshake1–3 ms (same rack)A reliable channel between client and database
TLS handshake (if enabled)2–10 msCertificate check, key negotiation
Authentication2–5 msThe database verifies user / password and privileges
Session initialization1–3 msCharset, time zone, connection properties
Totalabout 5–20 msA new connection repeats all of it
16 / 140

And an ordinary indexed query? Often under 1 ms. So if every request opened a connection, "establishing the connection" would cost more than ten times "doing the actual work" — the core contradiction.

17 / 140

Connection setup is expensive, yet the connection itself is a reusable long-lived channel. The natural optimisation follows: pre-create a batch of connections and recycle them — borrow when needed, return when done. That is the idea of pooling — database connection pools, thread pools, object pools, all the same trick: spread the cost of creating an expensive resource across N reuses.

18 / 140
Diagram
Figure 1 · Borrow, return and governance
Figure 1 · Borrow, return and governance
19 / 140
Section
2. DataSource and the implementation family
20 / 140

The previous article left a thread: JdbcTemplate never creates connections itself; it asks a DataSource. DataSource is the standard interface defined by the JDBC spec, and getConnection() is the whole contract — whether the returned connection is brand new or borrowed from a pool is none of the caller's business. That is the beauty of programming to an interface: switch the implementation and not a line of business code changes.

21 / 140

Four implementations dominate:

22 / 140
Table
ImplementationBoot defaultPerformanceMonitoringNotes / ecosystem
HikariCP✅ YesExtremely high (lock-free)Built-in Micrometer metrics + JMXLightweight, fast startup, few dependencies — the Boot favourite
DruidNoHighVery strong (built-in console + SQL firewall)From Alibaba; excels at monitoring and defence, many options
DBCP2NoMediumBasic JMXApache veteran; stable but conservative
Tomcat JDBC PoolNoHighBasic JMXSame origin as embedded Tomcat; async connection retrieval
23 / 140
Tip

since Spring Boot 2.x, HikariCP is the default pool — if it is on the classpath, DataSourceAutoConfiguration wires it automatically. So "which pool does Spring Boot use" is simply HikariCP, unless you explicitly set spring.datasource.type.

24 / 140
Section
3. Inside HikariCP: where the speed comes from
25 / 140

HikariCP is fast not through black magic but by driving every source of overhead in a high-concurrency pool down to near zero. The key designs:

26 / 140
Code
Codetext
HikariPool ├── ConcurrentBag<PoolEntry>     # connection store: ThreadLocal + lock-free stack, nearest-first ├── FastList<Statement>          # for closing statements: an ArrayList without bounds checks ├── ProxyConnection              # the connection handed out: proxies close / state validation └── HouseKeeper                  # background thread: keepalive, evict over-aged and idle connections
Notes
  • ConcurrentBag: the heart of the store. It combines "each borrower's own ThreadLocal list + a lock-free shared stack", so the same thread borrows and returns with almost no contention — a key reason it beats other pools
  • FastList: replaces ArrayList for holding a connection's Statements, dropping the bounds check and indexOf scan on every remove; closing statements is faster
  • ProxyConnection: the Connection you receive is actually a proxy. Calling close() does not really close the physical connection — the proxy returns it to the pool and resets its state, while also catching misuse such as using a connection after it is returned
  • HouseKeeper: a background thread that periodically does two things — pings idle connections (keepaliveTime) to keep them alive, and evicts connections past maxLifetime or idleTimeout, avoiding "dead" connections the database has already closed on its side

Key point: HikariCP's speed comes down to one line — use "lock-free + proxy + background governance" to remove every contention and system call from the borrow/return path, and move connection health checks off to a background thread.

27 / 140

All of those class names look familiar, yet each guards a different segment, and first-timers always mix them up. Play a round: pick a component, then pick what it actually saved you — a wrong pair explains itself on the spot.

28 / 140
Match
MatchMatch the HikariCP part to its jobMatched 0/6 · Missed 0
Six names you will meet again in Section 13; both columns are shuffled, so positions tell you nothing
Pick a card on the left first
29 / 140

And "the life of a connection" does not start at getConnection() nor end at close() — when it is born, examined and forcibly retired is all down to a background scheduler. Click through this ring; box ③ is the one you control:

30 / 140
Diagram
CycleThe life of one connection (click through it)1 / 6
Go from ① to ⑥ and back to ①: connections are not immortal, and the scheduler decides when one retires
→
→
→
→
→
↻
the HikariCP pool
① Birth: one full handshake
Creating a connection when the pool is empty pays the 5-20 ms bill from Section 1: TCP, authentication, session init. minimumIdle decides how many are pre-warmed, which is what saves the first wave of requests from queueing.
All clearOf the six boxes only ③ is decided by your code; the other five belong to the background thread.
31 / 140
Section
4. The full borrow-and-return flow
32 / 140

With the structure clear, one full "borrow and return" follows naturally:

33 / 140
Animation
Animation · How a connection gets borrowed
Animation · How a connection gets borrowed
34 / 140

That one is the pool's point of view. The animation below switches to a single request thread's point of view: the same getConnection() first checks its own thread's fast lane, then the shared stack, creates one only if nothing is free, and queues when the cap is hit — seven steps before you can see where a timeout actually comes from.

35 / 140
Animation
Animation · Inside one getConnection call
Animation · Inside one getConnection call
36 / 140
  1. The business code calls getConnection(): triggered inside JdbcTemplate before it runs SQL
  2. Look for an idle connection: ConcurrentBag first tries the current thread's local cache, then the shared area
  3. Hit: borrow directly: the PoolEntry is wrapped in a ProxyConnection and returned — you hold a proxy
  4. Miss and below the limit: create one: a full TCP + authentication round (the bill from Section 1)
  5. At the limit: queue and wait: no longer than connectionTimeout, then SQLTransientConnectionException
  6. Business calls close(): return it: the proxy intercepts close() and returns the connection to the pool (not closing it), resetting session state
37 / 140
类比|Analogy

this borrow-and-return path is exactly a library lending desk. There are only so many copies on the shelf (maximumPoolSize); the librarian never writes a new book in front of you, they hand over a copy that is on the shelf (an idle hit); when every copy is out you leave your name and wait to be called (queueing, pending); keeping a book forever with nobody chasing you = a leak; and returning a book destroys nothing — it simply puts the copy back into circulation. Returning is not burning; close() is not disconnecting. The ConnectionProxy is your library card: you feel like you are holding a card, while behind every swipe sits one real volume registered to your name.

38 / 140

The line to remember above all: in a connection pool, Connection.close() never means "disconnect" — it means "return". The physical connection is reused by the next request until evicted by maxLifetime / idleTimeout.

39 / 140
Code
Codejava
// This is why the following is correct: close in try-with-resources really means "return"try (Connection conn = dataSource.getConnection()) {    // ... run SQL}   // here the connection is returned to the pool; the physical connection stays alive
Notes
  • Business code's "close" is written exactly as before — this is the genius of the pool design: it is transparent to the upper layer
  • Only once truly returned can another thread borrow it; borrow without returning (Section 10) drains the pool
  • On return, HikariCP resets session state such as autoCommit and read-only flags, so the next borrower is not polluted
40 / 140
Section
5. Tuning the core parameters
41 / 140

Tuning a pool is really tuning the parameters below. Understand what each governs, then talk about values.

42 / 140
Table
ParameterMeaningDefaultRecommendation
maximumPoolSizeMaximum connections10Estimate from the formula; bigger is not better
minimumIdleMinimum idle connections= maxEqual to max avoids resize jitter
connectionTimeoutMax wait for a connection30000 ms3000–10000 ms; never 0 (infinite wait)
idleTimeoutIdle lifetime before eviction600000 msOnly effective when minimumIdle < maximumPoolSize
maxLifetimeMaximum connection lifetime1800000 msMust be less than the database wait_timeout
validationTimeoutConnection validation timeout5000 msShould be less than connectionTimeout
keepaliveTimeHeartbeat interval0 (off)Only meaningful below maxLifetime
43 / 140

Those seven knobs belong to two different worlds: the first four stand on the borrow path, and when they are wrong the application waits for a connection; the last three belong to housekeeping, and when they are wrong you receive a dead connection. Without that split, tuning is guesswork:

44 / 140
Diagram
Figure · Two knob groups: the borrow path vs housekeeping
Figure · Two knob groups: the borrow path vs housekeeping
45 / 140

So what should maximumPoolSize be? The HikariCP docs cite a counter-intuitive rule of thumb:

46 / 140
text
maximumPoolSize ≈ number of CPU cores × 2 + effective spindle count
47 / 140

Its meaning matters more than the number:

48 / 140
  • The bottleneck is usually disk IO and lock waits, not database CPU. More connections only make more threads queue for the disk; a bigger pool just lets more threads queue together
  • Connections are "concurrency", not "throughput". On an 8-core box, 16–20 is usually plenty; pushing it to hundreds just burns the database on context switching
  • The formula is a starting point, not an answer: the real work is load testing — begin at the formula value and adjust from the metrics
49 / 140

The formula is only a starting point; the feel comes from dragging the number. This console uses the article's own headline knob — slide it from 1 to 200 and the verdict changes all the way along. Watch the far right especially: "no queueing" and "faster" are not the same claim:

50 / 140
Tuner
TunerHow many connections should the pool hold
spring.datasource.hikari.maximum-pool-size
10connectionsNow 1 – 200
The Boot default: right for most small and medium systems
  • On a 4-8 core box, cores × 2 + spindles lands in exactly this range
  • pending stays at 0 while Threads_connected is nowhere near the limit
  • That default is not careless — it is the 'neither queueing nor stampeding' band
  • Before changing it, ask: is pending climbing, or is the database under pressure?
Queueing8%
Database load28%
Before you drag: this slider decides how many people get into the kitchen at once, not how fast the kitchen cooks.
51 / 140
Trap

the default maximumPoolSize of 10 is not "too small" — it is "right for most cases". Measure, then tune. Many "not enough connections" alerts trace back to a single non-indexed slow query; enlarging the pool only hides the problem.

52 / 140

A quick check (the first question is light, the second asks you to read a live incident):

53 / 140
Quiz
Check yourselfTrue or false: raising maximumPoolSize from 10 to 200 will definitely make the API faster.
Pick one — you get feedback right away
54 / 140
Section
6. Symptoms of a too-small vs too-large pool
55 / 140

Both extremes are dangerous, but they look completely different — the contrast points straight at the fix:

56 / 140
Table
Too smallToo large
SymptomsRequests queue, sporadic connectionTimeout, periodic stallsDatabase CPU / memory alarms, connections saturated
Key metricshikaricp.connections.pending ↑, acquire time ↑Database Threads_connected near its limit, context switches ↑
Chain reactionWeb thread pool fills up → site-wide outageDatabase buckles → every instance slows together
DirectionEnlarge moderately + hunt slow SQLShrink + optimise SQL / add caching
57 / 140
Note

"too small" is the app waiting for connections; "too large" is the database being crushed by them. The former is visible in the pool metrics; the latter requires looking at Threads_connected versus max_connections on the database side — and especially note that pool size × app instance count must not exceed the database max_connections, or scaling up becomes an incident.

58 / 140

Once you have diagnosed "queueing", the next knob decides how long you are willing to keep queueing. connectionTimeout is the one parameter that stays silent when misconfigured and then fails everywhere at once: too short and ordinary peak jitter gets killed; 0 and threads stand in that queue until the Tomcat pool is empty. Slide it and find the edges:

59 / 140
Tuner
TunerHow long will you wait for one connection
spring.datasource.hikari.connection-timeout
30000millisecondsNow 0 – 60000
The HikariCP default: generous, suits patient background work
  • 30 s is a default, not a recommendation — it comes from a 'do not fail easily' posture
  • A synchronous API waiting 30 s stretches every timeout upstream of it
  • Batch jobs and message consumers, where response time is not the point, can keep it
  • For online endpoints, lower it explicitly to 3-10 s and align it with the gateway
Wait time82%
Visibility80%
This slider is not about speed; it decides what you want to see when things go wrong — a bounded, explainable timeout beats a request line that never comes back.
60 / 140
Section
7. Monitoring: the pool's health report
61 / 140

You cannot tune what you cannot see. HikariCP exposes a set of metrics via Micrometer, queryable through Actuator:

62 / 140
Code
Codetext
# Currently active connectionsGET /actuator/metrics/hikaricp.connections.active# Threads currently waiting for a connection (sustained > 0 means the pool is small)GET /actuator/metrics/hikaricp.connections.pending# Connection acquisition time (the clearest sign of congestion)GET /actuator/metrics/hikaricp.connections.acquire# Current idle / total connectionsGET /actuator/metrics/hikaricp.connections.idleGET /actuator/metrics/hikaricp.connections.max
Notes
  • hikaricp.connections.pending is the most sensitive early-warning signal: sustained above 0 means requests are queueing, and the pool should grow (or the SQL should improve)
  • The percentiles (P99) of hikaricp.connections.acquire show jitter in acquisition directly
  • Correlate pool metrics with slow query logs: more slow SQL → longer hold times → rising pending. Once that causal chain lines up, the root cause is clear

Tip: these metrics require spring-boot-starter-actuator with the metrics endpoint exposed. In production, feed hikaricp.connections.* into Prometheus + Grafana and add an alert on "pending > 0 for 1 minute" — far earlier than a user complaint.

63 / 140
Section
8. A Spring Boot configuration example
64 / 140

A copy-ready config with every key parameter:

65 / 140
yaml
spring:  datasource:    url: jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai    username: root    password: ${DB_PASSWORD}    hikari:      pool-name: HikariPool-1      maximum-pool-size: 20      minimum-idle: 5               # let the pool shrink at off-peak and grow at peak      connection-timeout: 3000      # fail fast in 3s, don't leave threads hanging      idle-timeout: 600000          # recycle surplus connections after 10 idle minutes      max-lifetime: 1200000         # 20 minutes, must be less than MySQL wait_timeout      validation-timeout: 3000      keepalive-time: 300000        # a heartbeat every 5 minutes
66 / 140

At startup the log states the pool's status clearly:

67 / 140
Code
Codetext
HikariPool-1 - Starting...HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@5f1d2b3cHikariPool-1 - Start completed.
Notes
  • Starting...: the pool initialises and reads its configuration
  • Added connection ...: a warm-up connection was established (minimumIdle decides how many)
  • Start completed.: the pool is ready and getConnection() can borrow immediately
  • If it stalls here or reports a Timeout, it is almost always the url / credentials / network — for "cannot reach the database", look for these lines in the startup log first
68 / 140

That listing is someone else's answer. Which lines your own project needs is worth generating rather than copying: tick "datasource" alone for the minimum that connects, then add "logging" and "actuator" and watch how the hikaricp.connections.* metrics from Section 7 become queryable:

69 / 140
Generator
GeneratorGenerate the data-source block you actually needapplication.yml2 / 4
Tick datasource only and you get the minimum that connects; add logging to get SQL and timings; add actuator to make pending / acquire visible; profile shows the same file split by environment — in the prod one, turn leak-detection-threshold on
Output
server:
  port: 8080

spring:
  application:
    name: demo-service
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/bee_order?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: ${DB_USER:root}          # ${} 占位符:环境变量优先,冒号后是默认值
    password: ${DB_PASS:}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      max-lifetime: 1740000            # 必须小于 MySQL 的 wait_timeout
      pool-name: beeHikari

logging:
  level:
    root: INFO
    com.example.demoservice: DEBUG
    org.springframework.jdbc.core.JdbcTemplate: DEBUG   # 打 SQL 与参数
  file:
    name: logs/app.log
  logback:
    rollingpolicy: { max-file-size: 50MB, max-history: 14 }
Why each choice matters
datasourcePool settings only apply here; constructing HikariDataSource in code ignores every one of them.
loggingLevels work per package; root=DEBUG floods you with third-party output — never in production.
70 / 140
Warning

if the generated actuator block contains exposure.include: '*', edit it back to a whitelist. /actuator/env prints spring.datasource.password in plain text — that is exactly the incident the act lab in Section 12 demonstrates.

71 / 140
Section
9. Multiple data sources in one sentence
72 / 140

When you need read/write splitting or multiple databases, the idea is not "just configure several pools" but a routing data source switching between pools: extend AbstractRoutingDataSource and override determineCurrentLookupKey, keeping the current target in a ThreadLocal; or use dynamic-datasource-spring-boot-starter and annotate a method with @DS("slave"). The core insight is unchanged: each data source sits in front of its own HikariCP pool, and each pool size must be estimated separately.

73 / 140
Section
10. Two traps you will definitely hit
74 / 140
Trap

connection leaks — borrow without returning and the pool drains fast. The symptom is a burst of connectionTimeout after the app has run a while, recovered by a restart and recurring later. Two tools locate it: set spring.datasource.hikari.leak-detection-threshold=20000 (milliseconds) so a connection held longer than 20 seconds logs a stack-trace warning naming the borrower; or take a heap dump and follow the reference chain to ProxyConnection. The root cause is usually a missing try-with-resources, or a manual getConnection() where an early return on some exception branch skips close().

75 / 140
Trap

maxLifetime must be less than the database's wait_timeout. MySQL's default wait_timeout is 28800 seconds (8 hours) and it unilaterally closes connections that idle too long. If the pool's maxLifetime is larger, you get "HikariCP thinks the connection is alive while the database has already cut it", and the business hits Communications link failure on a dead connection. HikariCP's default maxLifetime of 30 minutes is usually safe; but once a DBA lowers wait_timeout, maxLifetime must be lowered too — leave margin so the pool evicts connections before the database does.

76 / 140

The second trap (maxLifetime above wait_timeout) shows up as "I received a dead connection". The first one — a leak — is a slow draining: nothing fails on the spot, which is precisely why nobody recognises it in time. Run these six frames together and the "a restart fixes it, then it recurs" fingerprint becomes something you have seen:

77 / 140
Animation
Animation · How a leak drains the pool
Animation · How a leak drains the pool
78 / 140

The second question reads a live scene (get this one wrong and production will hurt):

79 / 140
Quiz
Check yourselfEvery ten or twenty minutes a batch of connectionTimeout errors appears while hikaricp.connections.pending spikes yet active stays low. A restart fixes it instantly, then it recurs. Most likely cause and correct move?
Pick one — you get feedback right away
80 / 140
Section
11. Hands-on: the DataSource at work
81 / 140

The demo below shows how auto-configuration wires the DataSource. Toggle "the user has defined a DataSource" and watch @ConditionalOnMissingBean let auto-configuration step aside for the user's definition — exactly the mechanism that makes the Section 8 config work.

82 / 140
Kernel lab
83 / 140
Section
12. Take the pool apart: four kernel experiments plus two helpers
84 / 140

Everything above has been description; this section is hands-on. A connection pool is something you can push buttons on — the five demos below run the real kernel (WASM); click a parameter and it recomputes, so you can watch "idle hit", "create", "queue", "timeout" and "leak" as five genuinely different pictures. Suggested order: run the first to build intuition, then work down the list, and finish with the last two to understand production alerts.

85 / 140
Section
12.1 The whole borrow-and-return path (`pool`)
86 / 140

Start with the happy case: the pool holds an idle connection, getConnection() returns at once, and close() hands it back. Notice that the identity of the physical connection in the output never changes — that is reuse.

87 / 140
Kernel lab
TeaVMBorrow and return live: hit · create · queue · timeout · leakidle
Switch to "queue at the limit" and "wait timeout" to see the app stall, then look at the stack-trace warning under "leak detection"
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
88 / 140

How to read each parameter:

89 / 140
Table
ParameterWhat you seeReal-world counterpart
Idle hitReturns in microseconds; the same physical connection objectNormal state; the vast majority of requests take this road
Create when emptyAn extra TCP + authentication delay, and Added connection in the logCold start, too-small minimumIdle, or a traffic burst
Queue at the limitpending starts climbing while callers block in getConnection()Pool too small, or slow SQL holding connections
Wait timeoutSQLTransientConnectionException: Connection is not available, request timed out after 30004msThirty seconds of queueing with no turn — mass API failure
Leak detectionWith leakDetectionThreshold enabled, a stack-trace warning names who borrowed without returningThe classic "fine after restart, broken again later" symptom
90 / 140
Tip

these five are not parallel options but five stages of one chain. Click "queue" and "timeout" back to back and you have understood how ninety percent of pool alerts happen: slow SQL or a leak stops connections from circulating → queueing → the queue waits itself into a timeout.

91 / 140
Section
12.2 What JDBC does when there is no pool (`jdbc`)
92 / 140

JdbcTemplate never creates connections; it only asks a DataSource. Switch this demo to "what if you forget to release" and you will see the finally that hand-written JDBC most often omits: the moment an exception branch skips close(), that connection never comes back. This is exactly why try-with-resources exists.

93 / 140
Kernel lab
TeaVMThe JdbcTemplate skeleton and the missing closeidle
Switch to "forgetting to release" and spot which line should have returned the connection
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
94 / 140

Mapping for reference: one row = query, affected rows = update, batch = batch, ResultSet → object = map, leak = leak. The first four belong to article 27; in this article keep your eye on the last one: identical code, with and without finally / try-with-resources, gives the pool two completely different fates.

95 / 140
Section
12.3 Where the pool's health report comes from (`act`)
96 / 140

You cannot tune what you cannot see. spring-boot-starter-actuator turns those hikaricp.connections.* metrics from section 7 into clickable endpoints, and also demonstrates the risk of exposing everything — /actuator/env will happily print your database password.

97 / 140
Kernel lab
TeaVMActuator endpoints and health indicatorsidle
Switch to "everything open" to see what env leaks, then "metrics" to find hikaricp.connections.pending
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
98 / 140

The minimal beginner-safe configuration (three stanzas, copy and go):

99 / 140
yaml
management:  endpoints:    web:      exposure:        include: health,info,metrics     # only these three; never add env/shutdown in prod  endpoint:    health:      show-details: when_authorized       # reports DOWN plus the reason when the DB is unreachable
100 / 140

With that in place you can verify the sandbox numbers directly: curl localhost:8080/actuator/metrics/hikaricp.connections.pending.

101 / 140
Section
12.4 Why a transaction makes "getting a connection" simpler (`txprop`)
102 / 140

An often-missed fact: all statements inside one transaction reuse the same connection, because TransactionSynchronizationManager binds it to a ThreadLocal. So the code below borrows once and returns once:

103 / 140
java
@Transactionalpublic void transfer(Long from, Long to, BigDecimal amount) {    jdbc.update("UPDATE t_account SET balance = balance - ? WHERE id = ?", amount, from);    jdbc.update("UPDATE t_account SET balance = balance + ? WHERE id = ?", amount, to);    auditLog.record(from, to, amount);   // yet another SQL, still the same connection}
104 / 140

Change the propagation to REQUIRES_NEW or NOT_SUPPORTED and the number of borrows changes — which means the transaction boundary decides not only whether data rolls back, but also how many connections you hold and for how long.

105 / 140
Kernel lab
TeaVMBorrowing and returning under seven propagation typesidle
Compare REQUIRED with REQUIRES_NEW: the latter makes the inner method borrow its own connection
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
106 / 140
Trap

long transactions are a pool's number-one killer. An open transaction means a gripped connection, even while you call a third-party HTTP API or read a large file. If you spot a remote call inside a @Transactional method, move it outside immediately — otherwise the root cause of your peak-hour pending spike is that, not "the pool is too small".

107 / 140

Enough buttons — type the same things yourself. This console talks to the same in-browser Java kernel and every reply is computed there: start with beans to see which DataSource the container holds, then walk the five stages of a borrow with lab pool:

108 / 140
Console
109 / 140
Note

run lab pool queue and lab pool timeout back to back — the first is "already queueing but nothing has failed yet", the second is what the queue looks like when it expires. Read their output against the pending metric in Section 5 and the first row of the Section 13 table, and the numbers and the stack trace finally connect.

110 / 140
Section
13. Common errors quick lookup
111 / 140

What beginners fear is not the concepts but the wall of red text. This table quotes pool-related errors in a form you can search verbatim (with the key class names and fragments); just follow the third column.

112 / 140
Table
Error text (fragment)Actual cause30-second self-rescueDig deeper in
Connection is not available, request timed out after 30004ms (SQLTransientConnectionException)Thirty seconds of queueing with no connection: either the pool really is small, or slow SQL / leaks are holding themCheck whether hikaricp.connections.pending is above 0, then set leak-detection-threshold=20000 to reproduce; do not enlarge the pool firstSections 10 and 12
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failureThe pool thinks the connection is alive but the database closed it long ago (maxLifetime ≥ wait_timeout, or a firewall cut)Set max-lifetime clearly below MySQL's wait_timeout and enable keepalive-timeSection 10
ERROR 1040 (08004): Too many connectionspool size × instance count exceeds the database max_connections: scaling up became the incidentSum the pools across all instances, shrink them or add read replicas; ops can keep extra_max_connections in reserveSection 6
Failed to obtain JDBC Connection; nested exception is java.sql.SQLNonTransientConnectionException: Cannot load connection classWrong url / driver / credentials — the pool cannot reach the database at allLook in the startup log whether HikariPool-1 - Starting... reaches Start completed.; if not, it is the connection stringArticle 27
java.sql.SQLException: Connection is closed / was already closedThe reference was used after being returned (typically an async thread holding the caller's closed connection)Never pass connections between threads; re-acquire inside the child thread, or let the transaction manage itArticle 31
Apparent connection leak detected (from leakDetectionThreshold)A connection stayed borrowed past the threshold without being returned; the stack trace already names the culpritFollow the logged stack to the code missing try-with-resources — the fastest lead there isSection 10
The last packet successfully received from the server was N milliseconds agoAn idle connection was cut by the server timeout; same root as Communications link failureLower maxLifetime / raise keepaliveTime; autoReconnect on the URL is not a fallback strategySection 10
113 / 140
Tip

what these errors share is that they talk about connections while the disease usually lives in the SQL. So the order never changes: metrics (pending / acquire P99) → slow query log → leak stack → only then pool parameters.

114 / 140

Most beginners point at the wrong frame for that first row of the table, because the method at the top of the stack looks most guilty. Here is a real stack — do not read the answer, just click the frame you think is responsible:

115 / 140
Triage
Error triageSQLTransientConnectionException: Connection is not available
Mass connectionTimeout: the error is in the pool, the fault is in your code

A query endpoint that shipped two weeks ago starts returning 500s in clusters at peak. A restart fixes it instantly; forty minutes later it recurs, with the same sentence and four numbers in brackets.

java.sql.SQLTransientConnectionException: order-pool - Connection is not available, request timed out after 3000ms (total=2, active=2, idle=0, waiting=14)
at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696)
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:181)
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100)
at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:160)
at com.bee.order.repo.OrderRepository.findRecent(OrderRepository.java:57)
at com.bee.order.web.OrderController.recent(OrderController.java:34)
Click the frame you blame — guessing is allowed
No pressure: guess the exception first, then which line actually made the call.
116 / 140
Section
14. Hands-on exercises
117 / 140
Section
Tier 1 · Follow along: a pool you can watch queue in three minutes
118 / 140

Goal: run six concurrent tasks against a two-connection pool and see a real timeout exception once. Fully runnable code:

119 / 140
java
package com.example.pool;import com.zaxxer.hikari.HikariConfig;import com.zaxxer.hikari.HikariDataSource;import org.springframework.jdbc.core.JdbcTemplate;import java.util.concurrent.CountDownLatch;public class PoolDemo {    public static void main(String[] args) throws Exception {        HikariConfig cfg = new HikariConfig();        cfg.setJdbcUrl("jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai");        cfg.setUsername("root");        cfg.setPassword("secret");        cfg.setMaximumPoolSize(2);            // deliberately only 2        cfg.setMinimumIdle(2);                // fixed at 2, no resizing, easy to observe        cfg.setConnectionTimeout(3000);       // give up after 3s — fail fast        cfg.setPoolName("tiny-pool");        try (HikariDataSource ds = new HikariDataSource(cfg)) {            JdbcTemplate jdbc = new JdbcTemplate(ds);            CountDownLatch done = new CountDownLatch(6);            for (int i = 0; i < 6; i++) {          // 6 threads fighting over 2 connections                final int no = i;                new Thread(() -> {                    try (var conn = ds.getConnection()) {   // try-with-resources = return                        long start = System.currentTimeMillis();                        new JdbcTemplate(conn).queryForObject("SELECT SLEEP(2)", Integer.class);                        System.out.printf("[task-%d] borrowed and finished in %dms%n",                                no, System.currentTimeMillis() - start);                    } catch (Exception e) {                        System.out.printf("[task-%d] failed: %s: %s%n",                                no, e.getClass().getSimpleName(), e.getMessage());                    } finally {                        done.countDown();                    }                }, "worker-" + i).start();            }            done.await();            System.out.println("active=" + ds.getHikariPoolMXBean().getActiveConnections()                    + " idle=" + ds.getHikariPoolMXBean().getIdleConnections()                    + " waiting=" + ds.getHikariPoolMXBean().getThreadsWaiting());        }    }}
120 / 140

Expected console output (timestamps differ slightly, but the shape must match):

121 / 140
text
HikariCP version: 5.xtiny-pool - Starting...tiny-pool - Added connection com.mysql.cj.jdbc.ConnectionImpl@1a2b3c4dtiny-pool - Start completed.[task-0] borrowed and finished in 2005ms[task-1] borrowed and finished in 2006ms[task-2] borrowed and finished in 4012ms[task-3] borrowed and finished in 4015ms[task-4] failed: SQLTransientConnectionException: tiny-pool - Connection is not available, request timed out after 3004ms[task-5] failed: SQLTransientConnectionException: tiny-pool - Connection is not available, request timed out after 3003msactive=0 idle=2 waiting=0
122 / 140

Confirm four things against that output:

123 / 140
  • The first two finish almost together = each borrowed one connection (capacity 2)
  • Tasks 3 and 4 are late by exactly two seconds = they were queueing until earlier connections came back
  • Tasks 5 and 6 throw the very message from row one of the lookup table, and it carries the pool name tiny-pool — always name your pools in production, otherwise several pools failing together are impossible to tell apart
  • The trailing idle=2 proves nothing was destroyed; the connections merely went home
124 / 140
Section
Tier 2 · Variants: change three parameters, observe three diseases
125 / 140

Change one item at a time, rerun, and write down what you see:

126 / 140
  1. maximumPoolSize from 2 to 6 → you will observe: all six tasks finish around 2 seconds and waiting stays 0. That is the feel of the "just right" cell in the sandbox.
  2. maximumPoolSize to 200 with SELECT SLEEP(2) unchanged → you will observe: MySQL's SHOW STATUS LIKE 'Threads_connected' climbs steadily, database CPU rises, and your business is no faster — connections are concurrency, not throughput.
  3. Replace try (var conn = ...) with a manual var conn = ds.getConnection(); that never closes → you will observe: after a few rounds tiny-pool - Connection is not available appears, plus Apparent connection leak detected once leakDetectionThreshold is 1000. That is a leak in full: fewer connections each round, fixed by restart, recurring later.
  4. Add cfg.setMaxLifetime(60_000) and temporarily set MySQL wait_timeout to 30 seconds → you will observe: after some idleness, occasional Communications link failure. Conclusion: let the pool retire connections before the database does.
127 / 140
Section
Tier 3 · Build one: put a dashboard on a pool
128 / 140

Requirement: write a /orders/recent endpoint (JdbcTemplate, 20 orders) with an observation panel you can open in a browser. Acceptance checklist:

129 / 140
  • [ ] application.yml spells out six settings — pool-name, maximum-pool-size, connection-timeout, max-lifetime, keepalive-time, leak-detection-threshold — each with a comment saying what it governs (numbers alone do not count)
  • [ ] You can point out the three startup lines Starting... / Added connection / Start completed. and state how many connections were pre-warmed
  • [ ] With spring-boot-starter-actuator added, GET /actuator/metrics/hikaricp.connections.pending returns 0 while idle
  • [ ] Hammer the endpoint with 100 concurrent clients (JMeter / wrk / ab), setting maximum-pool-size to 2, 10 and 50 in turn, recording each time: P99 latency, peak pending, database Threads_connected
  • [ ] Write a five-line conclusion from those three runs: where is the knee? Why do more connections not help? Is the bottleneck in the app or the database?
  • [ ] Manufacture one deliberate leak (borrow without returning), locate the exact line through the leak-detection-threshold stack warning, and fix it
  • [ ] Bonus: chart the P99 of hikaricp.connections.acquire in Grafana and add an alert rule for "pending > 0 for 60 seconds"
130 / 140

Finish this tier and you have moved from "knowing a pool exists" to "operating one with data".

131 / 140
Section
15. Key-point self-check
132 / 140
Self-check

what does Connection.close() actually do inside a pool? If your answer was "disconnect the TCP", reread section 4 — it returns the connection and resets session state.

133 / 140
Self-check

what is the maximumPoolSize rule of thumb, and can you use it as the answer? cores × 2 + spindles is only a starting point; the verdict comes from load tests and the pending metric.

134 / 140
Self-check

two instances at 100 connections each, database max_connections is 151 — what happens? At peak you get Too many connections, and both instances suffer together. Burn in the hard constraint: pool size × instances < max_connections.

135 / 140
Self-check

pending stays above 0 — what is your first move? Hunt slow SQL and leaks first, not enlarge the pool; enlarging just hands the pressure to the database.

136 / 140
Self-check

between maxLifetime and MySQL's wait_timeout, which must be larger? wait_timeout — so the pool retires connections first and business never receives a dead one.

137 / 140

Mantra: **dialling is costly, talking is cheap; `close` returns, it does not disconnect. Small pools queue, big pools crush the database — catch slow SQL and leaks before you ever touch the pool size.**

138 / 140
Section
16. Decision and summary
139 / 140
Decision
Decisionproduction alerts fire with a flood of "connect timeout", and monitoring shows pool `pending` spiking. What is your first move?
140 / 140
Summary

remember a bill, a mechanism, a formula and a habit. The bill: "opening a connection costs about 5–20 ms while a query is often under 1 ms", so pooling is mandatory. The mechanism: "close() in a pool means return, not disconnect; HikariCP stays fast through a lock-free ConcurrentBag, a proxy and background governance". The formula: "maximumPoolSize ≈ cores × 2 + spindles; connections are concurrency, not throughput, and pool size × instances must not exceed the database's connection limit". The habit: "on an alert, always check leaks and slow SQL first — never blindly enlarge the pool". Treat the connection pool as an observable, tunable craft rather than a default value, and you remove half of all connection-related incidents.