How to Size Java Database Connection Pools in Kubernetes
Stop guessing your maximum-pool-size. Learn how to calculate limits backwards from your database

Senior Software Engineer. I write about practical tips and epic stories about my experiences in the field of computer science and engineering!
Introduction
I was reviewing a PR at work a month ago where a new application was setting up its database service, and I saw the connection pool properties, which made me think about how a small configuration looks in a single file, yet how destructive it can be at scale.
A database connection pool in Java looks like a small configuration:
spring:
datasource:
hikari:
maximum-pool-size: 10
Add a Kubernetes microservices environment, and that setting is not small at all.
It means: 10 database connections per pod
If the service runs 20 pods during peak traffic, the database can see:
20 pods x 10 connections = 200 possible connections
If a rolling deployment temporarily creates extra pods, or if a canary version runs beside the main deployment, the number can be higher.
That is the core mental model we will be discussing:
A Java connection pool is local to one pod, but the database pays the cost of all pools across all pods.
This post tries to explain how to size a Java JDBC connection pool in Kubernetes, how to choose maximumPoolSize, how to choose minimumIdle, how to reason about idleTimeout, and how to validate the settings in production.
The examples use Spring Boot with HikariCP because that is the common default stack for modern Java services. The same thinking applies to other JDBC pools.
1. What a connection pool really does
A connection pool has two jobs.
First, it is a cache. Creating a database connection is expensive. The application may need to create a TCP connection, authenticate, initialize session state, and allocate memory on the database side. A pool keeps ready-to-use connections so each request does not pay that cost.
Second, and more importantly, it limits how many concurrent database operations one pod can send to the database.
A larger pool does not always make a service faster. Once the database is already saturated, adding more connections often increases queueing, lock contention, memory pressure, and context switching.
Think of the pool like the number of checkout counters at a store. Too few counters and customers wait unnecessarily. Too many counters, and the store does not magically get more cashiers or shelves.
The goal is not to make the pool as large as possible. The goal is to use the smallest pool that keeps the database & application healthy while serving request SLAs.
HikariCP's own pool-sizing guidance is intentionally conservative: it emphasizes that optimal pools are usually much smaller than people expect and gives the classic starting point of approximately (core_count * 2) + effective_spindle_count active connections for database throughput testing. (See the references at the end for the official HikariCP pool-sizing page)
In the context of database connection pool sizing (like the HikariCP formula),
effective_spindle_countis essentially a measure of how many concurrent I/O request your server can manage.Rotating harddisks can only handle one I/O request at a time. If you have 16, your system can handle 16 I/O requests at the same time.
The mentioned rule of thumb is no longer valid for SSDs. As they can typically handle several parallel I/O requests at the same time.
Today the database is often CPU-bound, memory-bound, or lock-bound long before storage becomes the limiting factor.
This is a starting point for benchmarking and not a universal production formula.
2. Kubernetes changes the sizing problem
In a single JVM, the connection pool is easy to visualize: one application process → one connection pool → one database
In Kubernetes, the shape is different: many pods → many private pools → one shared database
If each pod has a pool of 10 and Kubernetes runs 12 pods, the service can open up to 120 database connections.
That is only the steady-state number. The real upper bound must include autoscaling, rolling updates, canary deployments, workers, jobs, and terminating pods.
Kubernetes Horizontal Pod Autoscaling adds more pods to match demand, and a Deployment rolling update can create pods above the desired replica count through maxSurge. Kubernetes defaults rolling updates to 25% maxSurge, which means extra pods can exist during rollout.
So this is the number that matters: total_possible_connections = max_possible_pods x maximumPoolSize_per_pod
3. Start from the database, not from Java
The better question to ask is: How many database connections can a service safely consume across all pods? Rather than asking how many connections my Java app should have?
The rule is to always start with the database's connection budget
As we know max_connections is the maximum number of concurrent connections to the server. Increasing it also increases server resources, so it should not be treated as a free knob. Databases like PostgreSQL also have reserved connection slots such as superuser_reserved_connections, which exist so administrators can still connect during emergencies.
A practical service-level budget looks like this:
usable_database_connections_for_a_service = database_max_connections minus a handful of reserved connections for things like:
reserved_admin_connections
other_services_connections
migration_connections
batch_job_connections
observability_and_tooling_connections
safety_margin
Example:
Database max_connections: 300
Admin/emergency reserve: 20
Other microservices: 60
Batch jobs and migrations: 15
Monitoring and tooling: 5
Safety margin: 40
Usable for this service: 160
That means this service should stay within roughly 160 database connections across all its pods.
The safety margin is not optional in Kubernetes. Pods can surge during rollouts. Pods can remain terminating. A canary can overlap with the main deployment. A retry storm can coincide with an autoscaling event. A database budget with no margin is a production incident waiting for a traffic spike.
Also,
max_connectionsitself should generally not be increased aggressively because every backend process/session consumes memory even when idle.
4. Calculate the maximum possible pod count
Now calculate the maximum realistic pod count:
max_possible_pods =
HPA_maxReplicas
+ rollout_maxSurge_pods
+ canary_overlap_pods
+ job_or_worker_pods_that_use_the_same_pool_settings
+ termination_overlap_buffer
Example:
HPA maxReplicas: 20
Deployment maxSurge 25%: 5
Canary pods: 2
Termination overlap buffer: 1
Max possible pods: 28
Now divide the database budget by the max possible pods: per_pod_database_cap = floor(usable_database_connections_for_this_service / max_possible_pods)
Using the example:
floor(160 / 28) = 5
So the database-budget-based upper bound is: maximum-pool-size: 5
If someone sets it to 20 because "20 seems normal", the real maximum becomes: 28 pods x 20 connections = 560 database connections
That is more than the entire database limit in this example. This is why pool sizing in Kubernetes must be done from the database backward.
5. Estimate how many connections the app actually needs
The database budget tells you the maximum safe cap. Now estimate application demand.
Use Little's Law: concurrent_database_connections_needed_per_pod ~= RPS_per_pod x DB_connection_hold_time_seconds
The important measurement here is not total HTTP request latency. It is the time the request holds a database connection.
For example:
Peak RPS per pod: 80 requests/second
Average DB connection hold time: 50 ms (0.050 seconds)
Concurrent DB need:
80 × 0.050 = 4
That means the pod needs about 4 active database connections on average during that peak.
But you should not run a pool at 100% utilization all the time. Add headroom. A common target is around 70-80% utilization: pool_size_from_demand = ceil(concurrent_DB_need / target_utilization)
Using 75%:
ceil(4 / 0.75) = 6
So the application-demand estimate says: maximum-pool-size: 6
Now compare the two constraints:
Database-budget cap: 5
Application-demand need: 6
The safe starting point is 5, but this also tells you something important: the application wants more concurrent database work than the database budget can safely provide.
Now that is not a pool tuning problem. That is a capacity or efficiency problem.
6. What if demand is greater than the database budget?
Suppose the math says:
Safe database cap per pod: 5
Application demand per pod: 12
Do not blindly set the pool to 12.
That would protect one pod's latency for a short time by pushing the bottleneck into the database. Under real traffic, the database will become the shared failure point for every service using it.
Instead, reduce connection demand or increase database capacity.
Ways to reduce demand:
Optimize slow queries.
Add required indexes.
Reduce transaction duration.
Avoid holding a DB connection while calling another service.
Reduce repeated queries inside loops.
Batch writes where safe.
Cache hot read paths.
Use read replicas for suitable read-heavy workloads.
There are ways to increase capacity, but they will come at a cost 💸💸:
Increase database CPU/memory/I/O capacity.
Separate workloads into different databases or clusters.
7. Understand maximumPoolSize
In HikariCP, maximumPoolSize is the maximum number of connections in the pool, including idle and in-use connections. When the pool reaches that size and no idle connection is available, callers to getConnection() wait up to connectionTimeout before failing.
So this: maximum-pool-size: 8 means that this pod may hold at most 8 database connections. It does not mean that this service may hold at most 8 database connections.
In Kubernetes, always mentally expand it: maximumPoolSize_per_pod x max_possible_pods
A good starting range for many synchronous Java services is: 4 to 10 connections per pod
That sounds small, but many services spend most of their request time doing CPU work, serialization, cache calls, network calls, or waiting on downstream services. The database connection is usually held for a smaller slice of the request.
8. Understand minimumIdle
minimumIdle is the number of idle connections HikariCP tries to keep ready in the pool.
There are two common modes.
Mode A: fixed-size pool
maximum-pool-size: 8
minimum-idle: 8
or simply omit minimumIdle, because HikariCP defaults it to the same value as maximumPoolSize.
A fixed pool keeps all connections warm. This is good when:
Traffic is steady.
Latency matters more than conserving idle DB connections.
Connection creation is expensive.
Pod count is predictable.
The database can afford the idle sessions.
The downside is that idle pods still hold database connections.
Example: 50 pods x minimumIdle 8 = 400 idle database connections
That can hurt the database even when traffic is low.
Mode B: elastic pool
maximum-pool-size: 8
minimum-idle: 2
idle-timeout: 180000
An elastic pool keeps a small warm baseline and grows under demand. After traffic drops, surplus idle connections above minimumIdle can be retired after idleTimeout.
This is good when:
Traffic is spiky.
HPA can create many pods.
Many services share one database.
Idle connections are causing pressure.
You prefer slower cold bursts over high idle connection count.
HikariCP recommends leaving minimumIdle unset unless you have a specific reason to maintain an elastic pool.
9. Understand idleTimeout
idleTimeout controls how long an unused connection may sit idle in the pool before HikariCP retires it.
But there is one critical rule: idleTimeout only matters when minimumIdle < maximumPoolSize
If you configure:
maximum-pool-size: 8
minimum-idle: 8
idle-timeout: 60000
then idleTimeout will not shrink the pool below 8. The pool is effectively fixed-size.
If you configure:
maximum-pool-size: 8
minimum-idle: 2
idle-timeout: 60000
then HikariCP can retire idle connections above 2 after they have been idle long enough.
If the timeout is too low, the application may repeatedly open and close connections, causing connection churn.
Connection churn can show up as:
More frequent authentication work.
More TLS handshakes.
More database backend process/session churn.
Higher latency after quiet periods.
More logs that look like intermittent connection noise.
10. Metrics to watch
Spring Boot can instrument available DataSource objects and expose metrics for active, idle, maximum, and minimum connections. Exact metric names vary by Spring Boot and Micrometer version, but the concepts are the same.
At the pool level, track:
active connections
idle connections
max connections
min connections
pending/waiting threads
connection acquisition time
connection timeout errors
connection creation rate
At the application level, track:
RPS per pod
p95/p99 HTTP latency
request error rate
request thread utilization
retry rate
At the database level, track:
total connections
active sessions
idle sessions
CPU utilization
I/O wait
lock waits
slow queries
deadlocks
transaction duration
replication lag, if applicable
Pool too small
Pending connection requests are frequently above zero.
Connection acquisition latency rises.
Hikari connection timeout exceptions occur.
Database still has spare CPU and low active-session pressure.
Pool too large
Database CPU is high.
Lock waits increase.
Query latency increases.
More connections are active but throughput does not improve.
App latency gets worse after increasing pool size.
11. Common anti-patterns
Anti-pattern 1: pool size equals web thread count
Bad:
Tomcat max threads = 200
Hikari maximumPoolSize = 200
That allows every request thread to hit the database at the same time. Most databases cannot efficiently execute that much concurrent work for one service.
Better:
Tomcat max threads = 200
Hikari maximumPoolSize = based on DB budget and measured DB hold time
Anti-pattern 2: sizing from current pods instead of max pods
Bad:
Current pods = 4
maximumPoolSize = 20
Total today = 80 connections
But the real environment might be:
HPA maxReplicas = 30
Deployment maxSurge = 25%
Possible pods ~= 38
38 x 20 = 760 possible connections
The database sees the worst case, not your screenshot from a quiet afternoon.
Anti-pattern 3: high minimumIdle everywhere
Bad:
maximum-pool-size: 20
minimum-idle: 20
on many services.
This creates many idle database sessions. Idle does not mean free. Idle connections still consume database memory, connection slots, and operational headroom.
Anti-pattern 4: increasing the pool when queries are slow
If queries are slow because of missing indexes, lock waits, I/O saturation, or bad query plans, a larger pool usually makes the pileup worse.
Fix query time first.
Anti-pattern 5: holding a connection during remote calls
Bad:
open transaction
write row
call payment service
call inventory service
commit transaction
The database connection stays open while the application waits on the network.
Better where possible:
call external services
open transaction
do short DB work
commit
Shorter connection hold time reduces the pool size needed for the same RPS
12. Production checklist
Before choosing the final values, ask these questions:
If every pod opens a maximumPoolSize number of connections, will the database survive?
If every pod keeps minimumIdle connections, will the database have too many idle sessions?
If the pool is exhausted, will the app fail fast or block long enough to cascade?
Is maxLifetime lower than any database/proxy/network connection lifetime?
Are we holding connections during external service calls?
Do load tests show throughput improvement when the pool increases?
Do database metrics stay healthy at the chosen size?
13. Final mental model
Use this picture whenever you configure database connections for Java in Kubernetes:
Database capacity is global.
Pods are multiplied.
Pools are per pod.
maximumPoolSizeis a per-pod bulkhead.minimumIdleis the idle connection cost per pod.idleTimeoutonly releases connections above minimumIdle.The best pool is usually the smallest pool that avoids unnecessary connection wait.
The most dangerous setting is not always a high maximumPoolSize. Sometimes it is a high minimumIdle across many pods.
The best configuration is not the one that makes one pod look fast in isolation. It is the one that keeps the whole system stable when Kubernetes scales, deploys, retries, and recovers.
In microservices, connection pooling is not just an application performance setting.
It is database capacity management.





