Liquibase Checksum Validation: Trust Boundaries, Safe Overrides, and Failure Modes
An architecture note on Liquibase changeset checksums: what the hash covers, the trust boundary it enforces, safe override patterns, and when to redesign around it.
18 Aug 2025, 03:46 UTC

Your deployment pipeline just failed with a checksum mismatch on a changeset that ran fine in staging last week. Someone edited a file that had already been applied, and Liquibase correctly refused to continue. That refusal is the feature, not the bug — this note explains what the checksum actually protects, the smallest safe ways to override it, and the conditions under which you should redesign around it.
What the checksum actually covers
When Liquibase applies a changeset, it computes a hash of the changeset's source content — the XML, YAML, or formatted SQL text — and stores it in the md5sum column of DATABASECHANGELOG. On every subsequent run it recomputes the hash and compares. A mismatch halts the deployment.
Two properties of this design matter operationally:
- The hash covers the text, not the generated DDL. Reformatting whitespace, reordering XML attributes, or changing line endings in a formatted SQL file changes the checksum even when the executed statements are byte-identical.
- The hash assumes the changelog file is the source of truth. If a DBA applies a hotfix directly on the database and edits the changeset to match, the stored checksum diverges and automation fails. That is intended drift detection.
Version note: Liquibase 4.25.0 changed the default algorithm from MD5 to SHA-256. Mixed-version environments (an old CLI in CI, a new one in production) can report false mismatches until every node upgrades. Verify the version on each runner before diagnosing anything else.
The smallest suitable design
The default behavior — append-only changelogs, checksum enforcement on, no overrides — is the right design for most teams. The discipline it enforces is simple: once a changeset has run anywhere, it is immutable. Corrections go in a new changeset.
Overrides exist for the cases where immutability has already been violated deliberately. In order of preference:
- Declare known-good alternatives with
validCheckSum. If a changeset was legitimately reformatted (e.g., during a file reorganization), add the new hash as an accepted value. Multiple entries are allowed, and the history remains auditable in the changelog itself. - Run
liquibase clearCheckSumsonce, in a coordinated window. This wipes all stored checksums so they are recomputed on the next deployment. Use it only after a reviewed, deliberate change — never as a routine pipeline step, because it disables drift detection for everything, not just the changeset you meant to fix.
Example of the first option, in XML:
<changeSet id="add-orders-table" author="alice">
<validCheckSum>8:abc123...</validCheckSum>
<validCheckSum>9:def456...</validCheckSum>
<createTable tableName="orders">...</createTable>
</changeSet>To find the value Liquibase currently computes, run (as a user with read access to the changelog, no database write needed):
liquibase calculateCheckSum \
--changelog-file=db/changelog.xml \
--changeset-id=add-orders-table \
--changeset-author=alicePaste that output into validCheckSum. Do not guess or hand-edit hashes.
Operational checks that catch problems early
Gate CI on validation against a clean schema copy, so checksum surprises never reach production:
liquibase --url=jdbc:postgresql://ci-db:5432/schema_copy validatevalidate exits non-zero on any checksum mismatch and prints the offending changeset's id and author. Run it in the pipeline stage that builds the changelog artifact, before any environment deployment.
When a mismatch does occur, inspect what is stored versus what is computed:
SELECT id, author, md5sum, dateexec
FROM DATABASECHANGELOG
ORDER BY dateexec DESC LIMIT 10;If the stored and computed values differ but the SQL looks identical, enable debug logging (-Dliquibase.log.level=DEBUG) to see the exact string being hashed. Whitespace, encoding (UTF-8 vs. UTF-8-BOM), and CRLF/LF differences are the usual culprits in formatted SQL files.
Failure modes
- Edited-in-place changeset. A developer fixes a typo in an already-deployed changeset. Deployment blocks. Correct fix: revert the edit, add a compensating changeset. Never "repair" history.
- False positive after tooling change. An editor normalized line endings across the repo. Fix with
validCheckSumentries or one auditedclearCheckSums, plus a.gitattributesrule to prevent recurrence. - Mixed Liquibase versions. Pre- and post-4.25.0 nodes disagree on algorithm. Fix by upgrading all runners; do not add checksums from both algorithms as a workaround.
- Manual hotfix drift. Intended detection. Reconcile by writing a changeset that represents the hotfix, marking it appropriately (e.g.,
markNextChangeSetRanor a precondition), not by clearing checksums.
Note that preconditions do not participate in the hash: a changeset whose precondition fails still has its checksum row written on a successful validation pass. Do not rely on preconditions to suppress checksum behavior.
When to change the design
Checksum enforcement assumes an append-only history. Two workflow changes break that assumption:
- Squashing changelogs before major releases. Rewriting history makes checksums noisy by construction. If you do this, pair the squash with a fresh baseline (a new
DATABASECHANGELOGinitialized from a snapshot) rather than fighting mismatches. - Frequent file reorganization. Use
logicalFilePathon changesets so moves and renames do not change identity, and considerrunOnChange="true"only for genuinely idempotent changesets (e.g., view definitions), where re-execution on content change is the desired semantics rather than an error.
Until those conditions apply, keep the default: immutable changesets, validation in CI, and validCheckSum as the only routine override.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.