# How to Size Java Database Connection Pools in Kubernetes

## 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:

```yaml
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.

![Kubernetes multiplies Java database pool size by pod count](https://cdn.hashnode.com/uploads/covers/5f40aec3cce6c969208c9d45/831384c2-1fc5-4309-9d38-a48f49250fb9.png align="center")

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.

1.  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**.
    
2.  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_count` is 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`

![](https://cdn.hashnode.com/uploads/covers/5f40aec3cce6c969208c9d45/4f50549c-ec16-405e-94aa-b74ffc898fcd.png align="center")

* * *

## 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:

```text
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_connections` itself 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:

```text
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:

```text
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:

```text
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](https://en.wikipedia.org/wiki/Little%27s_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%:

```text
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.

![Connection pool sizing workflow](https://cdn.hashnode.com/uploads/covers/5f40aec3cce6c969208c9d45/dc06f35f-ae92-4041-8e87-b853cf01fd3e.png align="center")

* * *

## 6\. What if demand is greater than the database budget?

Suppose the math says:

```text
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.

![](https://cdn.hashnode.com/uploads/covers/5f40aec3cce6c969208c9d45/0ec97ed0-05e4-47ac-86f5-9aa1dd56ab84.png align="center")

* * *

## 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

```yaml
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

```yaml
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.
    

![Fixed pool compared with elastic pool](https://cdn.hashnode.com/uploads/covers/5f40aec3cce6c969208c9d45/13373e2b-ac8a-4f96-87e0-59177c6edf03.png align="center")

> 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:

```yaml
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:

```yaml
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.
    

![Connection lifecycle inside a JDBC pool](https://cdn.hashnode.com/uploads/covers/5f40aec3cce6c969208c9d45/71da4b1e-e76a-4c6c-bb27-ec407b2a3575.png align="center")

* * *

## 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:

```text
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:

```text
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:

```text
Current pods = 4
maximumPoolSize = 20
Total today = 80 connections
```

But the real environment might be:

```text
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:

```yaml
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:

```text
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:

```text
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:

1.  If every pod opens a maximumPoolSize number of connections, will the database survive?
    
2.  If every pod keeps minimumIdle connections, will the database have too many idle sessions?
    
3.  If the pool is exhausted, will the app fail fast or block long enough to cascade?
    
4.  Is maxLifetime lower than any database/proxy/network connection lifetime?
    
5.  Are we holding connections during external service calls?
    
6.  Do load tests show throughput improvement when the pool increases?
    
7.  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.
    
*   `maximumPoolSize` is a per-pod bulkhead.
    
*   `minimumIdle` is the idle connection cost per pod.
    
*   `idleTimeout` only 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.

* * *

## References

*   [HikariCP Configuration](https://github.com/brettwooldridge/HikariCP)
    
*   [HikariCP About Pool Sizing](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing)
    
*   [Kubernetes Horizontal Pod Autoscaling](https://kubernetes.io/docs/concepts/workloads/autoscaling/horizontal-pod-autoscale/)
    
*   [PostgreSQL Connections and Authentication](https://www.postgresql.org/docs/current/runtime-config-connection.html)
