The Keycloak Upgrade Ledger

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

2026-08-26 · run 2026-08-26-index-precondition-anatomy

what Keycloak's index check actually checks, and the upgrade it kills

Upgrade
26.0.0 → 26.7.1
Scale
2,000,003 users, baseline-26.0.0-sqlseed-2m, Postgres 16 at stock settings, Hetzner CCX3
Database
Postgres 16 at stock settings, Hetzner CCX33 (8 vCPU dedicated, 30GB RAM, local NVMe
Topology
single container
Host
Hetzner CCX33 (fsn1) — 8 vCPU dedicated, 30GB RAM, local NVMe
Condition
routine — no adverse condition applied

Why this run existed

Run A established that Keycloak's index changeset is guarded by a name-based precondition and that the 300,000-row skip reads a planner statistic. It left four questions open, and each was cheap to answer once reltuples was known to be a lever. Answering them turned up a failure mode none of the previous twenty-one records contains: an upgrade that dies mid-migration and leaves the server unable to start.

Environment

Identical to 2026-08-25-runA-indexes-manually-created — 2,000,003 users, baseline-26.0.0-sqlseed-2m, Postgres 16 at stock settings, Hetzner CCX33 (8 vCPU dedicated, 30GB RAM, local NVMe), JVM -Xms1g -Xmx4g, official image, start with an external DB. Every leg begins with a verified restore of that baseline, so the legs are comparable to each other and to Run A's.

autovacuum is disabled on USER_ENTITY in the legs that write to pg_class, so an analyze cannot silently undo the setup between the write and the upgrade.

Results

LegSetupUpgradeexectypeIndex on USER_ENTITYWARNs
A6areltuples=1000, relpages honestREADYEXECUTEDbuilt, correct0
A6breltuples honest, relpages=100READYEXECUTEDabsent2
A7asame index name on the realm tableREADYEXECUTEDabsent1
A7bsame name, same table, UNIQUE, on (id)READYMARK_RANwrong index kept0
A7cA7a + falsified reltuplesFAILEDabsent0

Breakage observed

An index name collision on an unrelated table fails the upgrade and blocks startup

DatabaseIndexChecker reports a healthy database when the index is on the wrong table

Verification

ClaimPrimary sourceChecked
reltuples is the input; relpages is notA6a built, A6b skipped
Keycloak reads the raw value, not the planner's estimateRun A L5 — planner would compute ~533,920 and skip; index was built
Precondition ignores columnsRun A L3 — MARK_RAN on a single-column index
Precondition ignores uniquenessA7b — MARK_RAN on a UNIQUE index of a different column
Precondition honours the tableA7a — same name on realm, changeset ran
DatabaseIndexChecker does not honour the tablecontrol 2 WARNs vs A7a 1 WARN, message text captured
A name collision fails the upgradeA7c — Liquibase exception, quoted
It leaves 187/211 and migration_model 26.0.0A7c — queried directly
Restarting after clearing the collision resumes the changelognot tested this run — inferred from S8 and S14, which is not evidence
The threshold constant on USER_ENTITY is 300,000, inclusivebisected here — 300,000 builds, 300,001 skips
Behaviour on version pairs other than 26.0.0 → 26.7.1not tested

← All runs