Optimizing Keycloak's performance through JVM, database pool, and cache tuning, then monitoring with Prometheus metrics, Grafana dashboards, JMX, and capacity planning for user growth.

In episode 27 you built a fault-tolerant cluster. Episode 28 makes it fast and measurable: performance tuning so Keycloak serves many logins without ballooning, and monitoring so you know when to act before users complain. Tuning without metrics is guessing; metrics without tuning is watching.
Keycloak runs on the JVM, and memory is the first setting to pay attention to:
KC_HEAP variable. A heap that's too small triggers constant GC; too large wastes RAM and lengthens GC pauses.KC_HEAP=2g \
KC_DB_POOL_MIN_SIZE=5 \
KC_DB_POOL_MAX_SIZE=50 \
kc.sh startKC_HEAP=2g sets the memory limit; KC_DB_POOL_MIN_SIZE and KC_DB_POOL_MAX_SIZE govern the number of database connections. For a small instance start with 2 GB of heap; for enterprise loads with thousands of concurrent logins, raise it step by step while observing metrics. Every added cluster node (episode 27) also adds demand on the database — adjust the pool on both sides.
As a rule of thumb: change one variable at a time, then observe its impact on response time and GC. Changing heap, pool, and cache all at once leaves you unable to tell which change actually mattered.
Most of Keycloak's speed comes from the Infinispan cache. The relevant caches:
| Cache | Contents | Effect if too small |
|---|---|---|
realms | realm configuration | slow logins, frequent config reloads |
users | cached user profiles | more user queries to the database |
authorization | resources, policies, permissions | authorization evaluation slows down |
sessions | active sessions | users frequently logged out, sessions lost |
offlineSessions | offline sessions | offline tokens often fail |
The cache size must be enough to hold the active user population, but shouldn't waste memory. Cache invalidation is managed by Keycloak automatically when data changes; in a cluster, a change on one node is propagated via JGroups so the cache stays consistent. For very large loads, the session store can be moved to the database so memory doesn't fill up — a trade-off between speed and scale that you must decide based on measurement, not feelings.
Without observation, tuning has no direction. Enable Keycloak metrics:
KC_METRICS_ENABLED=true kc.sh start --optimizedWith KC_METRICS_ENABLED=true, Keycloak exposes Prometheus metrics at the /metrics endpoint. Prometheus then scrapes it periodically:
scrape_configs:
- job_name: keycloak
metrics_path: /metrics
static_configs:
- targets: ["sso.example.com:8080"]The metric data is then visualized with Grafana dashboards — many ready-to-use Keycloak community dashboards exist. For internal JVM details, use JMX (for example via JConsole or exporting to Prometheus with a JMX exporter). Centralized logging with ELK or Loki helps correlate across nodes; APM (Application Performance Monitoring) gives request traces from the load balancer down to the database.
Once metrics are flowing, set up alerting: alarms for a dropping login rate, a rising error rate, and a falling cache hit ratio are the three triggers that most often save a team from an incident. Grafana can be connected to Alertmanager or other notifications, so responses don't depend on whoever happens to open the dashboard.
Here are the metrics you should always watch:
| Metric | Meaning | Danger Sign |
|---|---|---|
| Login throughput | number of logins per second | Drops suddenly as load rises |
| Token generation rate | tokens issued per second | Spikes without cause |
| Response times | login and token endpoint latency | Keeps rising = a bottleneck |
| Error rates | percentage of failed requests | 5xx increasing = disruption |
| Cache hit ratios | proportion of accesses served from cache | Falling = wrong cache sizing |
| DB connection pool | database pool usage | Exhausted = requests queueing |
| JVM memory | used heap and GC frequency | Heap approaching the limit |
Besides Prometheus metrics, make use of the health endpoints already used in episode 27: /health/live for process liveness, /health/ready for readiness to serve, and /health/started for startup status. These health alarms are what inform the load balancer and your on-call team.
Monitoring answers "what's happening now"; capacity planning answers "what happens next month":
A recommended capacity planning process:
Tip
Store a baseline from normal load. During an incident, comparing metrics against the baseline is far more informative than looking at absolute numbers — for example "cache hit ratio dropped from 98 percent to 80 percent" immediately points to a cache sizing problem.
Episode 28 equipped your cluster with speed and vision: JVM tuning via KC_HEAP, database pool and cache; Prometheus metrics via KC_METRICS_ENABLED=true; Grafana visualization; JMX observation and centralized logging; key metrics; and trend-based capacity planning.
Key takeaways:
realms, users, and sessions caches.live, ready, started signal the load balancer and alerting.In the next episode (episode 29), you'll protect all this hard work: backup, disaster recovery & upgrades — from database and configuration backups, RTO and RPO, to safe version upgrades.