Keycloak Database Connection Pool Exhaustion: Causes and Fixes

Guilliano Molaire Guilliano Molaire 12 min read

Last updated: July 2026

Keycloak (Quarkus distribution) manages a JDBC connection pool via Agroal. “Too many connections” errors or a connection count that climbs to your database’s limit almost always trace back to one of three causes: the pool max-size is set too high (or not set at all), so max-size multiplied by the number of replicas exceeds PostgreSQL’s max_connections; connections are not recovered after a database blip because no validation query is configured; or the pool is growing to its maximum in response to load spikes and never fully releasing back down. Fix it by explicitly setting db-pool-initial-size, db-pool-min-size, and db-pool-max-size on every replica and ensuring the total across all replicas stays below your database’s max_connections, leaving headroom for admin tools and monitoring agents.

If you have hit this problem in production you are not alone. It surfaces repeatedly in the Keycloak community forums, and the symptoms range from intermittent HikariPool-1 - Connection is not available, request timed out errors to a full authentication outage where Keycloak logs FATAL: remaining connection slots are reserved for non-replication superuser connections coming back from PostgreSQL.

This guide explains exactly how Keycloak’s Quarkus-based connection pool works, what each pool knob does, how to size it correctly for clustered deployments, and how to diagnose the problem with real SQL and metrics queries.

How Keycloak Pools Database Connections in the Quarkus Distribution

Keycloak 17 and later run on Quarkus, which replaces the legacy WildFly/JBoss distribution. The Quarkus datasource layer uses Agroal as its JDBC connection pool implementation, not HikariCP. Agroal is tightly integrated with Quarkus’s reactive I/O model and exposes a small but precise set of configuration properties.

When Keycloak starts, Agroal opens db-pool-initial-size connections immediately. As load increases, it opens additional connections up to db-pool-max-size. When the pool is idle, Agroal can shrink it back down toward db-pool-min-size. Every connection in the pool maps directly to a backend connection in PostgreSQL’s pg_stat_activity view.

This matters for capacity planning: Agroal does not share connections across JVM threads the way a single-socket multiplexing pool (like PgBouncer in transaction mode) does. Every concurrent Keycloak thread that needs database access must acquire a dedicated connection from the pool. If all connections are in use, the thread blocks until the acquisition timeout is exceeded, at which point Keycloak throws an exception back to the caller.

The Three Pool Settings and What Each Does

Keycloak exposes the Agroal pool through three kc.sh options (and their corresponding environment variable equivalents):

CLI flag Env variable Default Description
--db-pool-initial-size KC_DB_POOL_INITIAL_SIZE 0 Connections opened at startup
--db-pool-min-size KC_DB_POOL_MIN_SIZE 0 Floor: Agroal will not shrink below this
--db-pool-max-size KC_DB_POOL_MAX_SIZE 100 Ceiling: Agroal will never exceed this

The default max-size of 100 is the single most dangerous default in a standard Keycloak installation. Most operators do not change it and deploy three or four replicas, which means Keycloak will attempt to open up to 400 connections to PostgreSQL. PostgreSQL’s own default max_connections is 100. The resulting contention causes the symptoms described in community threads repeatedly.

Setting these in practice

In kc.sh / keycloak.conf:

# keycloak.conf (Quarkus)
db=postgres
db-url=jdbc:postgresql://db-host:5432/keycloak
db-username=keycloak
db-password=secret

db-pool-initial-size=5
db-pool-min-size=5
db-pool-max-size=20

Using environment variables (Docker / Kubernetes):

# Kubernetes Deployment env block
env:
  - name: KC_DB
    value: postgres
  - name: KC_DB_URL
    value: jdbc:postgresql://db-host:5432/keycloak
  - name: KC_DB_USERNAME
    valueFrom:
      secretKeyRef:
        name: keycloak-db
        key: username
  - name: KC_DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: keycloak-db
        key: password
  - name: KC_DB_POOL_INITIAL_SIZE
    value: "5"
  - name: KC_DB_POOL_MIN_SIZE
    value: "5"
  - name: KC_DB_POOL_MAX_SIZE
    value: "20"

Note that all KC_DB_POOL_* values are strings in Kubernetes manifests even though they represent integers.

The Number-One Cause: max-size Times Replicas Exceeds max_connections

The arithmetic is simple but consistently overlooked:

Total possible connections = KC_DB_POOL_MAX_SIZE × number_of_replicas

With the default max-size=100 and three replicas that is 300. PostgreSQL’s default max_connections=100 means Keycloak will saturate your database the moment all replicas try to ramp up.

Even when operators raise PostgreSQL’s max_connections, they often do not account for:

  • PgBouncer or other pooler connections: If you run PgBouncer in session mode, each PgBouncer connection is still a dedicated Postgres backend.
  • Monitoring and DBA tools: pg_stat_activity, Datadog Postgres integration, and similar tools hold open connections.
  • Superuser reserve: PostgreSQL reserves superuser_reserved_connections (default: 3) connections that are unavailable to application users.
  • Replica lag readers: If you have read replicas or streaming replication slots, those consume connections too.

A safe formula for your Keycloak pool max per replica:

KC_DB_POOL_MAX_SIZE = floor(
  (postgres_max_connections - superuser_reserved - monitoring_connections - pgbouncer_admin_connections)
  / number_of_keycloak_replicas
)

Example: PostgreSQL max_connections=200, reserve 3 for superuser, 10 for monitoring/admin, 3 Keycloak replicas:

(200 - 3 - 10) / 3 = 62.3 → set KC_DB_POOL_MAX_SIZE=60

This gives each replica a ceiling of 60 connections and leaves 7 connections for maintenance headroom.

When to add PgBouncer

If your Keycloak cluster has more than four replicas, or if your PostgreSQL server has less than 2 GB of RAM, you should place PgBouncer (or pgpool-II) between Keycloak and PostgreSQL. PgBouncer in transaction-mode pooling lets many Keycloak connections multiplex onto fewer PostgreSQL backends.

Configure PgBouncer with pool_mode = transaction for Keycloak. Session-mode pooling provides no benefit for Keycloak’s usage pattern because Keycloak does not use advisory locks or session-level prepared statements in a way that requires session stickiness.

# pgbouncer.ini
[databases]
keycloak = host=postgres-host port=5432 dbname=keycloak pool_size=25

[pgbouncer]
pool_mode = transaction
max_client_conn = 500
default_pool_size = 25

With this configuration Keycloak can open up to 500 connections to PgBouncer while PgBouncer maintains only 25 real Postgres backends per database. See our PostgreSQL tuning guide for Keycloak for additional PgBouncer and max_connections guidance.

Behavior on Database Restart and Failover

When PostgreSQL restarts or a failover occurs, existing JDBC connections become stale. Agroal’s behavior at this point depends on whether you have configured connection validation.

Without validation, Agroal holds the stale connections in the pool and Keycloak threads block waiting for acquisition. When they eventually acquire a stale connection and attempt a query, the JDBC driver throws a PSQLException: An I/O error occurred while sending to the backend. Agroal then evicts that connection and opens a new one, but this happens serially per failed connection. Under load, this recovery can take minutes and causes an authentication outage that appears to users as login loops.

With validation configured, Agroal can detect broken connections proactively:

# keycloak.conf
db-pool-initial-size=5
db-pool-min-size=5
db-pool-max-size=20

# Validate connections before handing to caller (adds ~1ms overhead per acquire)
# Not directly exposed as a kc.sh flag; configure via quarkus.datasource.* in
# the Quarkus configuration file if your deployment uses it

For Keycloak’s Quarkus datasource, the background validation interval is exposed via the underlying Quarkus property:

# conf/quarkus.properties (create if it does not exist)
quarkus.datasource.jdbc.background-validation-interval=30s
quarkus.datasource.jdbc.acquisition-timeout=30s

The background-validation-interval causes Agroal to ping idle connections every 30 seconds and evict any that fail, so stale connections are cleaned up before a caller tries to use them. The acquisition-timeout prevents Keycloak threads from blocking indefinitely when all connections are in use.

For HA PostgreSQL setups (Patroni, AWS RDS Multi-AZ, Cloud SQL HA), combine this with a JDBC URL that includes the replica fallback hosts:

KC_DB_URL=jdbc:postgresql://primary-host:5432,replica-host:5432/keycloak?targetServerType=primary&loginTimeout=10&socketTimeout=30

The targetServerType=primary parameter instructs the JDBC driver to skip replicas and find the new primary after a failover. See the Keycloak Kubernetes production deployment guide for a full example of this pattern in a Kubernetes StatefulSet.

Apparent “Leaks” That Are Really Pool Growth to Max

A common misdiagnosis is calling this a “connection leak.” A true JDBC leak means a connection is acquired but never returned to the pool. Agroal has a leak detection mechanism (quarkus.datasource.jdbc.leak-detection-interval), but in most production cases what operators see is not a leak.

What they see is:

  1. Traffic spike hits Keycloak.
  2. Agroal opens connections toward max-size to service the load.
  3. Traffic drops, but Agroal keeps connections open down to min-size.
  4. Operator looks at pg_stat_activity and sees 60-80 idle connections per replica.
  5. They conclude there is a leak.

There is no leak. The pool is working as designed. The fix is to lower min-size to reduce the floor the pool rests at between peaks:

db-pool-initial-size=5
db-pool-min-size=5    # pool shrinks back toward 5 when idle
db-pool-max-size=30   # pool expands up to 30 under load

A true leak would show connections continuing to grow past max-size, which Agroal prevents by design. If you see Keycloak opening more connections than max-size allows, you are likely running multiple JVM processes per pod (never do this) or you have more replicas than you think.

Diagnosing Connection Pool Problems

Step 1: Check PostgreSQL pg_stat_activity

-- Count connections by application, state, and wait event
SELECT
  application_name,
  state,
  wait_event_type,
  wait_event,
  COUNT(*) AS connection_count
FROM pg_stat_activity
WHERE datname = 'keycloak'
GROUP BY application_name, state, wait_event_type, wait_event
ORDER BY connection_count DESC;

A healthy Keycloak deployment shows connections in idle state (parked in Agroal) and active state (executing queries). If you see many connections in idle in transaction state, a Keycloak thread is holding a transaction open for an unexpected duration, which warrants deeper investigation.

-- Find longest-running queries and transactions
SELECT
  pid,
  now() - pg_stat_activity.query_start AS duration,
  query,
  state
FROM pg_stat_activity
WHERE datname = 'keycloak'
  AND state != 'idle'
  AND query_start IS NOT NULL
ORDER BY duration DESC
LIMIT 20;
-- Check total connections vs max_connections
SELECT
  COUNT(*) AS active_connections,
  (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_connections,
  (SELECT setting::int FROM pg_settings WHERE name = 'superuser_reserved_connections') AS reserved,
  (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') - COUNT(*) AS available
FROM pg_stat_activity;

Step 2: Scrape Keycloak’s Agroal metrics

Since Keycloak 22, the management port (default: 9000) exposes a /metrics endpoint in Prometheus format. The Agroal pool metrics follow this pattern:

curl -s http://keycloak-pod:9000/metrics | grep -E "agroal|datasource"

Key metrics to watch:

Metric Description Alert threshold
agroal_datasource_pool_connections_max Pool ceiling (your max-size) Informational
agroal_datasource_pool_connections_active Connections currently in use >80% of max sustained
agroal_datasource_pool_connections_idle Connections parked in pool Unexpectedly high = over-provisioned
agroal_datasource_pool_connections_acquired_total Cumulative acquire count Rate spike = load spike
agroal_datasource_pool_connections_destroyed_total Evictions due to validation failure Spike = DB instability
agroal_datasource_pool_connections_invalid_total Connections failed validation >0 = stale connections detected
agroal_datasource_pool_acquire_timeout_total Acquire timeouts Any > 0 = pool exhaustion

A rising agroal_datasource_pool_acquire_timeout_total is the clearest signal of pool exhaustion. Set an alert on this metric in your observability stack. See the Keycloak production readiness checklist for a full list of recommended metrics and alert thresholds.

Step 3: Check Keycloak logs

Connection pool exhaustion surfaces in Keycloak logs as:

ERROR [io.agroal.pool] (executor-thread-2) Keycloak unavailable: javax.resource.ResourceException: IJ000453: Unable to get managed connection for java:/KeycloakDS

Or, if the JDBC acquisition timeout fires:

WARN  [io.agroal.pool] (executor-thread-5) Acquisition timeout of 30000ms exceeded

Both indicate the pool is at max-size and all connections are busy. Cross-reference the timestamp against your pg_stat_activity query to see what those connections were doing.

Symptom-to-Cause-to-Fix Reference Table

Symptom Likely Cause Fix
Connections climb to DB max_connections limit and Keycloak throws errors KC_DB_POOL_MAX_SIZE × replicas > max_connections Lower max-size per replica or add PgBouncer
Connections never release after traffic drops min-size is too high or equal to max-size Set min-size to 5 or 10% of max-size
Authentication outage lasting several minutes after DB restart No connection validation; stale connections in pool Set background-validation-interval=30s
agroal_datasource_pool_acquire_timeout_total rising during peaks Pool too small for peak load Increase max-size within per-replica budget
“Leak” suspected but connections stay below max-size Pool at min-size floor between peaks Expected behavior; lower min-size if needed
Connections in idle in transaction state in pg_stat_activity Long-running transaction holding connection Check for slow queries or misconfigured timeouts; set statement_timeout in PostgreSQL
DB failover causes minutes-long outage JDBC URL points to single host with no fallback Use multi-host JDBC URL with targetServerType=primary

These values are a starting point, not a prescription. Tune based on your observed pg_stat_activity and Agroal metrics.

Small deployment (1-2 replicas, < 500 users):

KC_DB_POOL_INITIAL_SIZE=3
KC_DB_POOL_MIN_SIZE=3
KC_DB_POOL_MAX_SIZE=15

Total max connections: 30 across 2 replicas. Works well with PostgreSQL max_connections=100.

Medium deployment (3-5 replicas, 500-10,000 users):

KC_DB_POOL_INITIAL_SIZE=5
KC_DB_POOL_MIN_SIZE=5
KC_DB_POOL_MAX_SIZE=25

Total max connections: 125 across 5 replicas. Set PostgreSQL max_connections=200 and reserve 60 connections for monitoring and superuser.

Large deployment (6+ replicas or autoscaling):

Add PgBouncer in transaction mode. Set KC_DB_POOL_MAX_SIZE=50 per replica and let PgBouncer maintain a fixed backend pool of 30-50 real Postgres connections regardless of how many Keycloak replicas are running.

The tuning process:

  1. Deploy with the conservative starting values above.
  2. Run load at 110-120% of expected peak.
  3. Monitor agroal_datasource_pool_connections_active vs agroal_datasource_pool_connections_max.
  4. If active connections never exceed 70% of max during load, your max-size has headroom to spare (or is too high across replicas).
  5. If active connections routinely hit 90%+ of max and acquire_timeout_total increments, increase max-size by 10, re-run the replica math, and adjust PgBouncer or max_connections as needed.
  6. After steady state, confirm pg_stat_activity idle count matches your min-size floor.

For Kubernetes autoscaling deployments, add the connection math to your HPA configuration: set a hard cap on replica count that keeps total connections within budget, and use the Agroal acquire_timeout_total metric as an HPA custom metric to trigger scale-out before exhaustion occurs.

Review the Keycloak cluster configuration best practices guide for how connection pool sizing fits into a broader cluster tuning strategy.

Frequently Asked Questions

How do I configure Keycloak’s database connection pool?

In the Quarkus distribution (Keycloak 17+), set db-pool-initial-size, db-pool-min-size, and db-pool-max-size in keycloak.conf or via the corresponding KC_DB_POOL_INITIAL_SIZE, KC_DB_POOL_MIN_SIZE, and KC_DB_POOL_MAX_SIZE environment variables. These map to Agroal pool properties and apply per JVM instance. The defaults (initial=0, min=0, max=100) are suitable for development but must be overridden in production.

Why does Keycloak open too many database connections?

The default max-size of 100 per replica is too high for most PostgreSQL deployments. In a three-replica cluster, Keycloak can open 300 connections against a database whose max_connections may be 100 or 200. Keycloak opens connections aggressively under load because Agroal grows the pool toward max-size whenever all current connections are busy. Size your pool so that max-size × replicas stays below your database limit, with headroom for monitoring and superuser connections.

Why do database connections not recover after a PostgreSQL restart?

By default, Agroal does not proactively validate idle connections. When PostgreSQL restarts, the TCP connections held by Agroal become stale. The pool does not know they are broken until a Keycloak thread tries to use one and receives a JDBC exception. Configure quarkus.datasource.jdbc.background-validation-interval (for example, 30s) in conf/quarkus.properties so Agroal pings and evicts stale connections proactively. Also use a multi-host JDBC URL so the JDBC driver can find the new primary after a failover.

Does Keycloak need PgBouncer?

Not always. For small to medium deployments (up to five replicas) where max-size × replicas fits within PostgreSQL’s max_connections budget, you do not need PgBouncer. For larger clusters, autoscaling deployments where the replica count varies, or PostgreSQL servers with limited RAM (each connection consumes 5-10 MB), PgBouncer in transaction-mode pooling significantly reduces the real connection count on PostgreSQL while allowing Keycloak’s pool to remain generous. Use PgBouncer when the replica math no longer fits within your PostgreSQL connection budget.

How do I tell the difference between a real connection leak and normal pool growth?

A true connection leak means connections grow beyond max-size. Agroal prevents this by design, so if your total connections in pg_stat_activity stay at or below max-size × replicas, you are not leaking. What you are likely seeing is the pool sitting at min-size connections per replica in idle state, which is expected behavior. A real leak would require a bug in Keycloak itself or a custom SPI provider that acquires JDBC resources without releasing them. Monitor agroal_datasource_pool_connections_destroyed_total and agroal_datasource_pool_connections_invalid_total via the /metrics endpoint on port 9000 to understand pool eviction patterns.

Conclusion

Database connection pool exhaustion in Keycloak almost always comes down to a sizing miscalculation: the default max-size of 100 was never intended to be used unchanged in a multi-replica production cluster. Set all three pool properties explicitly, do the replica math against your PostgreSQL max_connections, configure background connection validation so the pool recovers cleanly after database restarts, and monitor the Agroal metrics on the Keycloak management port.

For a broader look at keeping Keycloak healthy in production, see the Keycloak production readiness checklist, the Keycloak Kubernetes deployment guide, and the PostgreSQL tuning guide for Keycloak. If you need connection pool troubleshooting alongside other connectivity diagnostics, the Keycloak connection refused troubleshooting guide covers the broader network and port-binding failure modes.

Managing connection pool sizing across replicas is operational overhead that adds up fast. Skycloak’s managed Keycloak hosting handles pool configuration, PostgreSQL provisioning, and autoscaling for you, so your team can focus on integrations rather than infrastructure tuning.

Tired of running Keycloak yourself?

Skycloak runs real upstream Keycloak for you with a 99.99% SLA. No fork, no lock-in, just managed Keycloak that stays patched and on call so you don't have to.

Guilliano Molaire
Written by
Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Start Free Trial Talk to Sales
© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman