Database Connection Pool Tuning in Keycloak: The Bottleneck Nobody Notices Until Peak Load

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.

Simple Analogy

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.

i
Key Point

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.

Layer

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.

Layer

Agroal Connection Pool

Sits between Keycloak’s request-handling logic and the JDBC driver, managing a fixed set of reusable database connections.

Layer

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.

Layer

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:

SettingWhat It ControlsDefault
db-pool-initial-sizeHow many connections are opened immediately at startupNot set by default
db-pool-min-sizeThe minimum number of connections the pool keeps open even when idleNot set by default
db-pool-max-sizeThe hard ceiling on simultaneous connections the pool will ever hold100
!
Common Misconception

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
        
FIG 1 — What happens when a request asks Agroal for a database connection

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.

Simple Analogy

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.

1

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.

2

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.

3

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.

4

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.

5

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.

i
Key Point

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.

!
Common Mistake

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

5s
is a commonly cited acquisition-timeout threshold before requests start failing
In-Use
connection count to trend against pool max-size
Wait Time
for connection acquisition as a leading indicator

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.

i
Key Point

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.

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.

ANTI-PATTERN-01 Avoid
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.

ANTI-PATTERN-02 Avoid
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.

Practice

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.

Practice

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.

Practice

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.

Practice

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.

ScenarioTuning Focus
Multi-site HA deploymentMatching initial, min, and max pool sizes for predictable startup behavior
Kubernetes with strict CPU limitsCoordinating thread pool and connection pool sizing against CPU throttling risk
Shared database across multiple applicationsAllocating a fair connection budget per consuming application

11Frequently Asked Questions

Q1What’s the difference between db-pool-min-size and db-pool-initial-size?

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.

Q2Is the default maximum pool size of 100 safe to leave unchanged?

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.

Q3What happens when a request can’t get a connection in time?

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.

Q4Should I tune the connection pool and thread pool at the same time?

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.

Q5How do I know if my connection pool is actually the bottleneck?

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, and db-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.”