The Keycloak Upgrade Ledger

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

2026-08-25 · run 2026-08-25-26.0.0-to-26.7.1-3

26.0.0 → 26.7.1 (rollback exercised)

Upgrade
26.0.0 → 26.7.1
Scale
1000 users, typical profile, 2 realms, 10 clients, 8 roles
Database
Postgres 16.15 single node
Topology
single container, single replica
Host
Hetzner CCX33 (fsn1) — 8 vCPU dedicated, 30GB usable RAM, 226GB local NVMe, Debian 13, kernel 6.12.101+deb13-cloud-amd64
Rehearsal
attempt 3 of 3 — this is the rollback run the gate requires — this lab holds a path to three clean runs, one exercising rollback
Condition
routine — no adverse condition applied

Environment

Identical to attempts 1 and 2: Hetzner CCX33 (fsn1), 8 vCPU dedicated, 30GB RAM, local NVMe; Postgres 16.15 single node; 1000 users, typical profile, 2 realms, 10 clients, 8 roles; JVM -Xms1g -Xmx4g; single container.

Procedure

restore baseline-26.0.0-typical-1k          # identical starting state
upgrade 26.0.0 -> 26.7.1, wait ready
POST /admin/realms/lab/users {canary-postupgrade}   # data written AFTER the snapshot
# --- roll back ---
stop keycloak; KC_VERSION=26.0.0
dropdb / createdb / pg_restore baseline-26.0.0-typical-1k
start keycloak 26.0.0, wait ready

The canary is the point of the run. Anything written between the snapshot and the rollback decision is what a customer actually loses, and asserting that it disappears is the only way to know the rollback is real rather than nominal.

Timings

PhaseDuration
DB drop + create + pg_restore2.475s
Total, rollback decision → serving traffic again15.9s

The restore is 2.5s of the 15.9s. The rest is Keycloak process start, which is the same fixed cost seen everywhere else in these runs.

Outcome

Breakage observed

None. The rollback was clean.

Rollback

NOTE added 2026-08-25: this run performed the restore steps by hand with docker compose up -d --force-recreate. bin/restore.sh as shipped at the time used docker compose start, which reuses the existing container and silently re-migrates — see 2026-08-25-backup-and-rollback-tooling. The finding below holds for the procedure tested; the script did not implement it.

Verification

ClaimPrimary sourceChecked
Schema reverted, not just the binaryMIGRATION_MODEL read from Postgres directly = 26.0.0
Post-snapshot writes are lostcanary user, asserted absent after rollback
Keycloak has no supported schema downgradenot verified against docs — inferred from the restore being required at all
Holds at scalenot tested — 2.5s restore is a 500K dump

Publishable extract

Rolling back a Keycloak upgrade is a database restore, and it costs you every write since the snapshot.

On a 1,000-user realm (Postgres 16.15, Hetzner CCX33 — 8 vCPU dedicated, 32 GB, local NVMe), rolling 26.7.1 back to 26.0.0 took 15.9 seconds end to end. Only 2.5s of that was the pg_restore; the rest was Keycloak starting.

We verified it is a real rollback and not a cosmetic one: MIGRATION_MODEL in Postgres went back to 26.0.0, so the schema itself reverted. We also created a user after the upgrade and confirmed it was gone afterwards — because that is the part people miss. There is no schema downgrade. You are restoring a point-in-time backup, so every registration, password change, session and admin edit between your snapshot and your rollback decision is discarded.

The practical consequence: your rollback window is not bounded by how long the restore takes. It is bounded by how much write activity you can afford to throw away. Decide that number before the maintenance window, because during one you will not want to.

Publishable. The 15.9s figure is honest but must always be quoted with its 1,000-user scale attached — the restore half scales with the dump, the startup half does not.

← All runs