Database Connection Pool Tuning in Keycloak: The Bottleneck Nobody Notices Until Peak Load
Keycloak talks to its database on almost every login, token refresh, and admin action. How many database connections it's allowed to hold open at once — and how it's configured to behave when that limit is reached — quietly decides whether your identity server sails through a traffic spike or grinds to a halt.
Picture a busy bank branch with only four teller windows, no matter how many customers are waiting. On a quiet Tuesday morning, four windows is plenty — nobody waits more than a minute. But on the first day of the month, when everyone shows up to deposit a paycheck at once, that same four-window limit turns into a slow-moving queue out the door, even though the bank itself hasn’t changed at all. A database connection pool is Keycloak’s teller-window count: a fixed, configured limit on how many conversations with the database can happen at the same instant, regardless of how many login requests are arriving.
1What Is a Database Connection Pool?
Before tuning anything, it helps to understand why Keycloak doesn’t just open a fresh database connection for every single request.
Why not open a new connection every time?
Opening a brand-new connection to a database is surprisingly expensive: it involves a network handshake, authentication, and setup work on both the application and database side. If Keycloak opened and closed a new connection for every login attempt, most of the work involved in handling that request would be spent on connection setup rather than the actual login logic — and under heavy traffic, the database itself would struggle just to keep up with the churn of connections opening and closing.
What a pool does instead
A connection pool solves this by opening a batch of database connections once, up front, and keeping them open and ready to reuse. When Keycloak needs to talk to the database, it borrows an already-open connection from the pool, uses it, and returns it — instead of creating and destroying a connection each time. Keycloak, being built on the Quarkus framework, uses a connection pooling library called Agroal to manage this pool internally.
Think of a connection pool like a car rental counter that keeps a fixed number of cars ready in the lot, rather than manufacturing a brand-new car every time a customer walks in. Customers borrow a car, return it when they’re done, and the next customer reuses it. The size of that fixed lot — not the counter staff, not the paperwork — is what decides how many customers can be served in parallel.
Why “tuning” is even necessary
The pool has a fixed size, and that size is a trade-off. Too small, and requests start queuing up waiting for a free connection the moment traffic increases, even if the database itself has spare capacity. Too large, and Keycloak may open more simultaneous connections than the database can comfortably handle, straining the database server instead of Keycloak. Tuning is the process of finding the size — and related timing settings — that fits your actual traffic and your actual database’s capacity.
A connection pool problem often looks like “Keycloak is slow” or “logins are timing out,” even though the real bottleneck has nothing to do with Keycloak’s own processing logic — it’s requests waiting in line for a database connection that isn’t available yet.
2Architecture: Where the Pool Sits
The connection pool doesn’t operate alone — it’s one link in a chain that starts with an incoming HTTP request and ends at the physical database server.
HTTP / Executor Thread Pool
Handles incoming requests to Keycloak itself, sized based on available CPU cores and configurable via the http-pool-max-threads option.
Agroal Connection Pool
Sits between Keycloak’s request-handling logic and the JDBC driver, managing a fixed set of reusable database connections.
JDBC Driver
The database-vendor-specific library (for PostgreSQL, MySQL, Oracle, and others) that actually speaks the database’s wire protocol over each pooled connection.
Database Server
The physical or managed database instance, which has its own separate limit on how many total connections it will accept from all clients combined.
The three core pool-size settings
Keycloak exposes the underlying Agroal pool’s sizing directly as top-level configuration options:
| Setting | What It Controls | Default |
|---|---|---|
db-pool-initial-size | How many connections are opened immediately at startup | Not set by default |
db-pool-min-size | The minimum number of connections the pool keeps open even when idle | Not set by default |
db-pool-max-size | The hard ceiling on simultaneous connections the pool will ever hold | 100 |
The default maximum pool size of 100 is not automatically “correct” for your deployment. It’s a generic starting point — the right number depends entirely on your database server’s own connection limit, how many Keycloak instances you’re running, and your actual traffic pattern.
3Internal Working: What Happens When a Request Needs a Connection
Understanding this sequence explains exactly where things go wrong when the pool is undersized.
The borrow-and-return cycle
When a piece of Keycloak’s request-handling code needs to read or write to the database, it asks the Agroal pool for a connection. If a free connection is already sitting in the pool, it’s handed over immediately. If none are free but the pool hasn’t yet reached its maximum size, Agroal opens a brand-new connection on the spot. If the pool is already at its maximum and every connection is currently in use, the request has to wait.
sequenceDiagram
autonumber
participant R as Incoming Request
participant P as Agroal Pool
participant DB as Database
R->>P: Request a connection
alt Free connection available
P-->>R: Hand over existing connection
else Pool below max-size
P->>DB: Open a new connection
P-->>R: Hand over new connection
else Pool at max-size, none free
P-->>R: Wait in queue (acquisition timeout applies)
end
R->>DB: Execute query using the connection
R->>P: Return connection to pool
The acquisition timeout — the moment things start failing
A request that’s waiting for a free connection doesn’t wait forever. Once it can’t acquire one within a configured timeout window, the request fails outright rather than hanging indefinitely. This is a deliberate design choice: a system that let requests queue endlessly for a database connection would eventually pile up so many waiting requests that it collapses under its own memory and thread usage, rather than failing a manageable number of individual requests quickly.
This is like a restaurant with a strict “we’ll only hold your table reservation for 10 minutes past your booking time” policy. It seems harsh to the person turned away, but without that rule, a chain of late arrivals would eventually back up the entire evening’s seating for every other guest.
Why thread pool size and connection pool size are linked
Keycloak’s HTTP thread pool determines how many requests can be actively processed in parallel in the first place. If the thread pool is large but the database connection pool is small, a burst of concurrent requests will quickly exhaust the available connections, and everything past the connection pool’s limit queues up waiting — even though there was theoretically enough thread capacity to handle the requests. Sizing these two pools in isolation from each other is one of the most common tuning mistakes.
4Data Flow & Lifecycle of a Tuning Exercise
Tuning a connection pool isn’t a one-time guess — it follows a measure-adjust-verify cycle.
Establish a Baseline Under Realistic Load
Before changing any setting, load-test Keycloak with traffic resembling your expected peak, and record response times, error rates, and how close the pool gets to its configured maximum.
Identify Whether the Pool Is the Bottleneck
Check whether requests are failing or slowing down specifically while waiting for a database connection, as opposed to being slow for unrelated reasons such as CPU exhaustion or a slow query.
Check the Database’s Own Connection Ceiling
Confirm how many total connections the database server itself allows across all Keycloak instances and any other applications sharing it, since this is the real upper bound the pool size can’t exceed.
Adjust Pool Size and Related Timeouts
db-pool-min-size, db-pool-max-size, and db-pool-initial-size are adjusted together, alongside the HTTP thread pool size, to reflect the measured bottleneck rather than a guess.
Re-test and Repeat
The same load test is repeated to confirm the change actually improved throughput and latency without simply shifting the bottleneck to the database server itself.
Tuning in multiple Keycloak instances at once matters just as much as tuning one. If you run five Keycloak nodes behind a load balancer, each with a max pool size of 100, your database needs to be able to sustain up to 500 simultaneous connections, not 100.
5Advantages and Trade-offs of Different Pool Sizes
Advantages of a Well-Sized Pool
- Requests get a database connection quickly under normal and peak load, avoiding queuing delays.
- The database server isn’t overwhelmed by more simultaneous connections than it can efficiently manage.
- Predictable behavior under stress: failures happen fast and clearly via the acquisition timeout, rather than as a slow, confusing degradation.
- Resource usage on both Keycloak and the database stays proportional to actual need, rather than either idling on unused capacity or straining under an undersized limit.
Trade-offs to Manage
- A pool set too small causes otherwise-healthy requests to fail or slow down purely due to connection queuing, even when Keycloak’s own CPU and memory have headroom.
- A pool set too large per instance, multiplied across several Keycloak nodes, can exceed what the database server is actually configured to accept.
- Keeping a high minimum pool size holds database resources open even during quiet periods, which has a real (if usually small) resource cost on the database side.
- Correct sizing depends on real traffic data, so early guesses often need revisiting as usage patterns change.
6Security Considerations
Connection pool exhaustion as a denial-of-service vector
An undersized or misconfigured connection pool can turn ordinary traffic spikes — or a deliberate flood of authentication requests — into a full denial-of-service condition, since every legitimate login attempt competes for the same limited set of connections. Rate limiting and brute-force protection at the authentication layer work alongside pool sizing, not instead of it, to keep the system resilient under hostile traffic.
Assuming that network-level rate limiting alone protects against connection pool exhaustion. A relatively modest but sustained volume of authentication attempts can still saturate a small connection pool long before any broader network-level defense would trigger.
Credentials and connection encryption still apply per pooled connection
Every connection inside the pool authenticates to the database using the same configured credentials and, in production deployments, the same TLS settings, such as a database TLS mode and trust store path. Tuning pool size doesn’t change these security properties, but it’s worth confirming that a larger pool hasn’t been sized without also confirming the database server can sustain that many encrypted connections without excessive overhead.
Shared databases and blast radius
If a database server is shared across several applications or Keycloak instances, an oversized pool from one consumer can starve the others of connections, effectively causing a self-inflicted denial of service against unrelated systems. Pool sizing decisions on a shared database need to account for every consumer’s fair share, not just Keycloak’s own preference.
7Monitoring, Logging & Metrics
What to watch under normal operation
The most direct signal is how close the number of connections actually in use gets to the configured maximum during real traffic. A pool that regularly sits near its ceiling during normal peak hours, with no room to absorb an unexpected spike, is a strong sign that the maximum needs raising — assuming the database can support it.
Metrics-enabled visibility
Keycloak can expose operational metrics, including connection-pool-related figures, when metrics are enabled on the server. These integrate naturally with standard monitoring stacks, letting connection pool saturation, acquisition wait times, and overall request latency be viewed together on the same dashboard rather than diagnosed after the fact from logs alone.
Once a request fails to acquire a connection within the acquisition timeout, it fails outright rather than degrading gracefully. This means connection pool exhaustion tends to show up suddenly as a spike in error rates, rather than as a gradual slowdown — which is exactly why proactive monitoring of pool utilization matters more than reactive monitoring of failures alone.
Correlating with database-side metrics
Pool metrics on the Keycloak side should always be read alongside the database server’s own connection count and load metrics. A pool that looks healthy from Keycloak’s perspective can still be contributing to database-side congestion if several Keycloak instances’ pools collectively approach the database’s total connection ceiling.
8Design Patterns & Anti-Patterns
Start Low, Load-Test, and Raise Gradually
Rather than guessing a large pool size upfront, begin with a conservative setting, measure real behavior under representative load, and increase incrementally — verifying at each step that the database server comfortably absorbs the new ceiling.
Size the Pool Per Instance, Then Multiply
In a multi-node Keycloak deployment, pool sizing is planned as “per-instance maximum times number of instances,” checked against the database’s total connection limit, rather than each instance being sized in isolation.
Problem
Raising db-pool-max-size as a first response to any performance complaint, without first confirming the connection pool is actually the bottleneck.
Why It’s Harmful
If the real bottleneck is CPU, a slow query, or the HTTP thread pool, increasing the connection pool does nothing to fix the underlying issue and can push the database server past its own comfortable connection limit for no benefit.
Correct Approach
Measure where requests are actually waiting — connection acquisition, query execution, or thread scheduling — before changing any single setting in isolation.
Problem
Sizing each Keycloak instance’s connection pool independently in a multi-node cluster without checking the combined total against the database’s own connection limit.
Why It’s Harmful
Five instances each configured at the default maximum of 100 can request up to 500 simultaneous connections, which many database configurations are not provisioned to handle, leading to database-side connection refusals under peak load.
Correct Approach
Divide the database’s total safe connection budget across all Keycloak instances deliberately, leaving headroom for other applications sharing the same database.
9Best Practices & Common Mistakes
Load-test with realistic traffic patterns
Synthetic tests that don’t resemble your actual login and token-refresh mix can lead to a pool size that looks fine in testing but fails under real usage.
Tune the HTTP thread pool and connection pool together
Treat http-pool-max-threads and the db-pool-* settings as one coordinated decision, not two unrelated knobs.
Account for every Keycloak instance in the cluster
Multiply per-instance pool size by node count when checking against the database’s own connection ceiling.
Revisit sizing as traffic grows
A pool size that fit your traffic a year ago may be undersized today — treat it as a setting to revisit periodically, not a one-time decision.
Common mistakes to avoid
Beyond the anti-patterns above, teams often forget to check the database server’s own maximum connection setting before raising Keycloak’s pool size, discovering the mismatch only when the database starts rejecting new connections outright. It’s also common to leave db-pool-min-size unset while running in an environment with strict cold-start latency requirements, missing out on the benefit of having connections already warm and ready the moment traffic arrives.
10Real-World & Industry Examples
High-Availability, Multi-Site Deployments
Organizations running Keycloak across multiple geographic sites for high availability explicitly document setting db-pool-initial-size, db-pool-min-size, and db-pool-max-size to matching values as part of their production sizing procedure, ensuring predictable, pre-warmed pool behavior from the moment each node starts.
Kubernetes-Based Deployments Under CPU Limits
Teams running Keycloak in Kubernetes pay particular attention to keeping thread and connection pool sizes proportional to the CPU limits assigned to each pod, since exceeding the CPU allowance causes throttling — a form of congestion that can look identical to a connection pool problem from the outside.
Shared Database Clusters Serving Multiple Applications
When a database cluster serves Keycloak alongside other applications, platform teams commonly allocate an explicit connection budget per application to prevent any single one — Keycloak included — from starving the others during its own peak load.
| Scenario | Tuning Focus |
|---|---|
| Multi-site HA deployment | Matching initial, min, and max pool sizes for predictable startup behavior |
| Kubernetes with strict CPU limits | Coordinating thread pool and connection pool sizing against CPU throttling risk |
| Shared database across multiple applications | Allocating a fair connection budget per consuming application |
11Frequently Asked Questions
db-pool-initial-size controls how many connections are opened immediately when Keycloak starts up. db-pool-min-size controls the floor the pool will shrink back down to during idle periods, keeping at least that many connections open even when demand drops.
It depends entirely on your database’s own connection limit and how many Keycloak instances you run. In a single-instance deployment against a database with generous connection limits, it may be perfectly fine. In a multi-node cluster, it can add up quickly and needs to be checked against the database’s total capacity.
The request fails outright once it exceeds the connection acquisition timeout, rather than waiting indefinitely. This is intentional — it prevents an unbounded backlog of waiting requests from destabilizing the whole system.
Yes. They’re closely linked — a large thread pool with a small connection pool just moves the bottleneck from thread scheduling to connection queuing. Treat their sizing as one coordinated decision informed by the same load testing.
Look for requests specifically slowing down or failing while waiting to acquire a connection, and check how close in-use connections get to the configured maximum under load. If those numbers stay comfortably below the ceiling while performance still suffers, the bottleneck is likely elsewhere — CPU, a slow query, or thread scheduling.
12Summary and Key Takeaways
Database connection pool tuning in Keycloak is the practice of sizing the fixed set of reusable database connections — managed internally by Agroal — so that real traffic gets served quickly without overwhelming the underlying database server. It’s controlled through db-pool-initial-size, db-pool-min-size, and db-pool-max-size, works hand-in-hand with the HTTP thread pool, and fails predictably through a connection acquisition timeout rather than degrading silently. Good tuning is never guessed in isolation — it comes from measuring real load, checking the database’s own connection ceiling, and accounting for every Keycloak instance in the cluster before settling on a number.
Key Takeaways
- A connection pool avoids the cost of opening a new database connection per request — reusing a fixed set of already-open connections instead.
- Keycloak uses Agroal under the hood, configured through
db-pool-initial-size,db-pool-min-size, anddb-pool-max-size(default 100). - The connection pool and HTTP thread pool must be tuned together — sizing one without the other just moves the bottleneck.
- Requests fail fast, not slow, when the pool is exhausted — an acquisition timeout deliberately prevents unbounded waiting.
- Multi-node clusters multiply pool size — the database’s real load is the per-instance maximum times the number of instances.
- Always check the database’s own connection ceiling — Keycloak’s pool size means nothing if the database itself can’t sustain it.
- Tune from measurement, not guesswork — load-test, identify the real bottleneck, adjust, and re-test before calling any pool size “correct.”