The Keycloak Upgrade Ledger

Every upgrade we have rehearsed, with the environment stated and the clock running.

2026-09-01 · run 2026-09-01-ha-sync-and-clean-failover

Postgres HA: sync replication + clean (promote-while-up) failover

Scale
3 realms (master, lab, ha-sync), seeded in-place
Database
PostgreSQL 16, primary + synchronous streaming replica, containers
Topology
1 primary (db) + 1 replica (db-replica); Keycloak single container
Host
laptop (Docker 29, Compose 5.5.0)
Condition
Maintenance · failover

Summary

This closes the last two untested legs of the Postgres HA line (2026-08-26-ha-failover-mid-migration covered the crash case). Two results:

  1. Synchronous replication works as advertised. With synchronous_commit=remote_apply + synchronous_standby_names=* the replica reports sync_state=sync, idle replay lag ~44 ms right after the clone settling to ~0.7 ms in steady state, and a committed realm write is immediately visible on the hot standby — the primary does not ack until the standby has applied the WAL.
  2. A clean failover has a different failure shape than a crash. Promoting the replica while the primary is alive leaves the old primary running with sync replication but no standby — so its writes block indefinitely (the probe CREATE TABLE never completed). That is a safety property (the old primary cannot accept split-brain writes), but it means the old primary wedges until stopped. The switchover must therefore be: promote → repoint Keycloak → stop the old primary, and an operator who promotes first without quiescing writes will find the old primary hung, not failed.

Environment

FieldValue
Keycloak26.7.3 (official image), start w/ external DB
DatabasePostgreSQL 16, primary + synchronous streaming replica, containers
Replicationwal_level=replica, synchronous_commit=remote_apply, synchronous_standby_names=*, streaming via pg_basebackup -X stream
Topology1 primary (db) + 1 replica (db-replica); Keycloak single container
Dataset3 realms (master, lab, ha-sync), seeded in-place
Hostlaptop (Docker 29, Compose 5.5.0)
JVM heap-Xms1g -Xmx4g

Procedure

COMPOSE_FILE=docker-compose.yml:docker-compose.ha.yml
HA_SYNC_COMMIT=remote_apply HA_SYNC_STANDBYS='*'
docker compose up -d db                       # primary, sync settings
./bin/ha-init-replica.sh                       # clone + start standby
docker compose up -d keycloak                  # writes now ack only via replica
# create a realm to prove write visibility on the standby
POST /admin/realms {"realm":"ha-sync"}

# clean failover: promote while the primary is STILL UP
docker compose exec -u postgres db-replica pg_ctl promote -D /var/lib/postgresql/data
# probe: write to the OLD primary (sync, no standby) -> BLOCKS, CREATE TABLE never commits

# switchover
sed KC_DB_URL -> jdbc:postgresql://db-replica:5432/keycloak
docker compose up -d --force-recreate keycloak  # repoint
docker compose stop db                          # break split brain

Results

Sync replication (steady state)

SignalValue
pg_stat_replication.sync_statesync (priority 1)
replay lag right after clone~44 ms
replay lag steady state (after a write)~0.7 ms
realm write visible on standbyha-sync present on the replica immediately ☑

Clean failover (promote-while-up)

StepObservation
pg_ctl promote on replicapromoted cleanly, out of recovery
Old primary (alive) after promotionstill reports itself a primary (pg_is_in_recovery()=f)
Write to old primaryblockedCREATE TABLE sync_block_probe never committed (pg_tables count 0); session hung waiting for a sync standby
Repoint Keycloak → replicaready; realms ha-sync, lab, master all present ☑
Stop old primarysplit brain broken; single primary remains

What this teaches

Verification

ClaimEvidence
Replica is a synchronous standbypg_stat_replication: sync_state=sync, sync_priority=1
Primary does not ack before standby appliesrealm write visible on replica, replay lag ~0.7 ms ☑
Old primary blocks writes after promoteCREATE TABLE probe never committed; session hung ☑
Keycloak resumes on promoted replicaready; ha-sync/lab/master realms intact ☑
No split brain at endold primary stopped; one primary (db-replica) ☑

← Back to the Ledger