Troubleshooting: 10 Common Liquibase Errors and How to Fix Them
Liquibase errors tend to cluster around the same ten problems. You will hit most of them at least once. This article gives you the exact error message to search for, the root cause, and the fastest fix — so you spend minutes recovering, not hours debugging.
Error 1: DATABASECHANGELOGLOCK — “Waiting for changelog lock”
Symptoms
Waiting for changelog lock....
Waiting for changelog lock....
Waiting for changelog lock....
liquibase.exception.LockException: Could not acquire change log lock.
Or the application hangs at startup and never finishes booting.
Root Cause
When Liquibase runs, it sets LOCKED=1 in the DATABASECHANGELOGLOCK table. When the run completes, it sets it back to LOCKED=0. If the process is killed mid-run (OOM kill, network timeout, Ctrl+C), the lock is never released — and every subsequent run waits forever.
Fix
Option A — Use the release-locks command (safest):
liquibase release-locks \
--url=jdbc:mysql://localhost:3306/ecommerce \
--username=liquibase_user \
--password=secret
Option B — Direct SQL (if release-locks can’t run):
UPDATE DATABASECHANGELOGLOCK
SET LOCKED = 0, LOCKGRANTED = NULL, LOCKEDBY = NULL
WHERE ID = 1;
Verify the lock is clear:
SELECT * FROM DATABASECHANGELOGLOCK;
-- LOCKED should be 0
Prevention
- Set a lock wait timeout so Liquibase gives up after N minutes instead of waiting forever:
spring.liquibase.database-change-log-lock-wait-time=2m - In Kubernetes, add a pre-stop hook that runs
liquibase release-locksbefore the pod is terminated - Never kill a running migration process — let it complete or fail cleanly
Error 2: Checksum Validation Failed
Symptoms
Validation Failed:
1 change(s) have invalid checksums
db/changelog/v1.0/1.0-create-users.yaml::001-create-users::abhay was:
8:a1b2c3d4... but is now: 8:e5f6g7h8...
Root Cause
Liquibase stores a checksum of each changeset when it runs. If the changeset file is modified after it has been applied — even a whitespace change — the next run sees a different checksum and refuses to proceed.
Common causes:
- Editor auto-formatting changed indentation
- Line ending conversion (CRLF vs LF)
- Property substitution changed a value
- Someone edited a deployed changeset “just to fix a typo”
Fix
Option A — If the modification was accidental (whitespace/encoding only), revert it:
git diff db/changelog/v1.0/1.0-create-users.yaml
git checkout db/changelog/v1.0/1.0-create-users.yaml
Option B — If the change was intentional and you want to accept the new checksum:
Update the checksum to NULL in the tracking table (Liquibase will recalculate):
UPDATE DATABASECHANGELOG
SET MD5SUM = NULL
WHERE ID = '001-create-users'
AND AUTHOR = 'abhay'
AND FILENAME = 'db/changelog/v1.0/1.0-create-users.yaml';
Or use the clearCheckSums command (clears all checksums — use with care):
liquibase clearCheckSums \
--url=jdbc:mysql://localhost:3306/ecommerce \
--username=liquibase_user \
--password=secret
Option C — Add validCheckSum to the changeset (permanent bypass):
- changeSet:
id: 001-create-users
author: abhay
validCheckSum:
- ANY # accept any checksum forever
changes:
- createTable:
tableName: users
Use ANY only when you’re certain the changeset hasn’t changed semantically.
Option D — Calculate the current checksum to add explicitly:
liquibase calculate-checksum \
--changeset-identifier="db/changelog/v1.0/1.0-create-users.yaml::001-create-users::abhay"
Then add that specific value to validCheckSum instead of ANY.
Prevention
- Never modify a deployed changeset. Add a new changeset instead.
- Configure
.editorconfigto enforce consistent line endings across your team:[*.yaml] end_of_line = lf - In CI, run
liquibase validateas the first pipeline step to catch checksum mismatches before attempting deployment
Error 3: “Class was not specified and could not be determined”
Symptoms
Unexpected error running Liquibase: liquibase.exception.DatabaseException:
Class was not specified and could not be determined.
Or:
liquibase.exception.DatabaseException: Cannot find database driver:
com.mysql.cj.jdbc.Driver
Root Cause
The MySQL JDBC driver JAR is not on the classpath. This happens when:
- Running Liquibase CLI without placing the driver in the
lib/directory - Maven/Gradle dependency scope is
runtimebut the build doesn’t package it correctly - Using the wrong driver class name
Fix
For Liquibase CLI:
Download mysql-connector-java-8.x.x.jar and place it in:
liquibase/lib/mysql-connector-java-8.x.x.jar
For Maven:
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
<scope>runtime</scope>
</dependency>
For the Maven Liquibase plugin specifically:
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>4.27.0</version>
<dependencies>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
</dependencies>
</plugin>
The plugin needs the driver as its own dependency — the project dependency doesn’t automatically carry over.
Verify the driver class name:
| MySQL Version | Driver Class |
|---|---|
| MySQL 8.x (modern) | com.mysql.cj.jdbc.Driver |
| MySQL 5.x (legacy) | com.mysql.jdbc.Driver |
In liquibase.properties:
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/ecommerce?useSSL=false&serverTimezone=UTC
Error 4: Duplicate Changeset Identifiers
Symptoms
[ERROR] Validation Failed
1 change(s) have duplicate identifiers
db/changelog/v1.1/feature-a.yaml::001-add-column::developer1
db/changelog/v1.1/feature-b.yaml::001-add-column::developer1
Root Cause
Two changesets have the same id + author + filepath combination. This often happens when two developers copy-paste a template on the same day and don’t update the ID.
Fix
Rename the duplicate in the newer file:
# Before (duplicate)
- changeSet:
id: 001-add-column
author: developer1
# After (fixed)
- changeSet:
id: 2026-05-10-001-add-preferences-column
author: developer1
If the duplicate is already deployed (one of them ran successfully), mark the other as ignored:
- changeSet:
id: 001-add-column
author: developer1
ignore: true # Liquibase skips this entirely
Prevention
Use timestamp-based IDs. The format YYYY-MM-DD-NNN-description almost guarantees uniqueness:
2026-05-10-001-add-user-preferences
2026-05-10-002-add-product-tags
Two developers working on the same day can increment the NNN counter. In code review, a duplicate ID is immediately visible.
Error 5: Changeset Exists in DB but Not in Changelog
Symptoms
liquibase.exception.MigrationFailedException: Migration failed for changeset
db/changelog/v1.0/1.0-baseline.yaml::001-create-users::abhay:
Reason: liquibase.exception.DatabaseException: Table 'users' already exists
The table exists in the database, but the changeset doesn’t have a precondition to handle it.
Root Cause
This happens during adoption (the table was created before Liquibase) or when someone manually ran SQL against the database without creating a changeset.
Fix
Option A — Mark the changeset as already ran (without executing it):
liquibase mark-next-changeset-ran \
--url=jdbc:mysql://localhost:3306/ecommerce \
--username=liquibase_user \
--password=secret \
--changelog-file=db/changelog/db.changelog-master.yaml
Or target a specific changeset:
liquibase mark-changeset-ran \
--changeset-identifier="db/changelog/v1.0/1.0-baseline.yaml::001-create-users::abhay"
Option B — Add a precondition to the changeset itself:
- changeSet:
id: 001-create-users
author: abhay
preConditions:
onFail: MARK_RAN
not:
- tableExists:
tableName: users
changes:
- createTable:
tableName: users
# ...
Prevention
During database adoption (Article 22), always add onFail: MARK_RAN preconditions to every baseline changeset. For new changesets, use onFail: HALT (the default) so failures are explicit.
Error 6: Rollback Not Supported
Symptoms
liquibase.exception.RollbackImpossibleException:
No inverse to sql created for Change.
Or:
Unexpected error running Liquibase: No inverse to dropTable created
Root Cause
Liquibase cannot automatically generate rollback SQL for:
sql/sqlFilechangesets (raw SQL)dropTable,dropColumn,dropIndexinsert,update,deletedata changes- Any change where the reverse operation requires knowing the prior state
Fix
Add an explicit rollback block to every changeset that needs it:
- changeSet:
id: 2026-05-10-001-drop-legacy-column
author: abhay
changes:
- dropColumn:
tableName: users
columnName: legacy_field
rollback:
- addColumn:
tableName: users
columns:
- column:
name: legacy_field
type: VARCHAR(255)
For raw SQL changesets:
- changeSet:
id: 2026-05-10-002-update-status-values
author: abhay
changes:
- sql:
sql: "UPDATE orders SET status = 'PENDING' WHERE status = 'NEW'"
rollback:
- sql:
sql: "UPDATE orders SET status = 'NEW' WHERE status = 'PENDING'"
To explicitly mark a changeset as having no rollback (use sparingly):
rollback:
- empty: {}
Prevention
Write the rollback block at the same time you write the changes block — it’s much harder to reconstruct rollback logic after the fact. In CI, run liquibase future-rollback-sql to verify rollback works before deployment.
Error 7: Out-of-Order Changeset Execution
Symptoms
Changesets run in the wrong order, causing foreign key failures:
liquibase.exception.DatabaseException: Cannot add foreign key constraint
(table 'order_items' references 'orders' which doesn't exist yet)
Or with includeAll, a new file runs before an older one it depends on.
Root Cause
includeAll processes files alphabetically. If you name files inconsistently, the order may not match logical dependencies.
Example of a broken naming scheme:
add-order-items.yaml # runs first alphabetically
add-orders.yaml # runs second — but order_items depends on orders!
Fix
Rename files with numeric prefixes:
001-create-orders.yaml
002-create-order-items.yaml # now runs after orders
Or switch from includeAll to explicit include statements:
databaseChangeLog:
- include:
file: db/changelog/v1.1/001-create-orders.yaml
- include:
file: db/changelog/v1.1/002-create-order-items.yaml
Explicit includes always run in the listed order, regardless of filename.
Prevention
Adopt a strict naming convention from the start:
YYYY-MM-DD-NNN-description.yaml
Files sort correctly by date, and the NNN counter handles same-day ordering. Never use plain descriptive names like add-orders.yaml with includeAll.
Error 8: precondition Failure Blocking Migration
Symptoms
ERROR: Preconditions failed
db/changelog/v1.1/001-add-index.yaml::001-add-index::abhay
Precondition failed with message: index 'idx_users_email' already exists
Root Cause
A precondition with onFail: HALT (the default) blocks the entire migration when the precondition fails. Common scenario: an index already exists because it was created manually, and the changeset tries to create it again.
Fix
If the object already exists and the changeset should be skipped:
Change onFail to MARK_RAN:
preConditions:
onFail: MARK_RAN
not:
- indexExists:
tableName: users
indexName: idx_users_email
If the object shouldn’t exist but does (real conflict):
Investigate whether someone ran DDL outside of Liquibase. If the object is correct, add the precondition. If it’s wrong, drop the object and let the changeset recreate it properly.
onFail values quick reference:
| Value | Behavior |
|---|---|
HALT (default) | Stop everything, fail with error |
CONTINUE | Skip this changeset, keep running others |
MARK_RAN | Record as executed, skip actual execution |
WARN | Log a warning, keep running |
Use HALT for new migrations (failures should be loud). Use MARK_RAN for baseline/adoption changesets.
Error 9: includeAll Picks Up Wrong Files
Symptoms
Unexpected changesets running, or migration files being processed in a confusing order. Sometimes test fixture files get applied to production.
ERROR: Unexpected table 'test_users_fixture' in production database
Root Cause
includeAll without a filter includes every file in the directory matching the format. Test data files, README files, or anything that matches *.yaml will be included.
Fix
Option A — Use the filter attribute to restrict by filename pattern:
- includeAll:
path: db/changelog/migrations/
filter: .*migration.*\.yaml
Option B — Use resourceComparator to control ordering:
- includeAll:
path: db/changelog/migrations/
resourceComparator: liquibase.changelog.StandardChangeLogComparator
Option C — Move test/seed data to a separate directory and use contexts:
db/changelog/
├── migrations/ ← includeAll targets this
│ ├── 001-create-tables.yaml
│ └── 002-add-indexes.yaml
└── seed/ ← only included in dev/test contexts
└── 001-test-data.yaml
Then in the master:
databaseChangeLog:
- includeAll:
path: db/changelog/migrations/
- changeSet:
id: seed-test-data
author: abhay
context: dev,test
changes:
# seed data here
Error 10: Spring Boot Startup Fails — Liquibase Can’t Connect
Symptoms
BeanCreationException: Error creating bean with name 'liquibase':
Invocation of init method failed; nested exception is
liquibase.exception.DatabaseException: Connection could not be acquired from driver
Root Cause
Spring Boot can’t establish a database connection before running Liquibase. Common causes:
- Database is not yet available (startup race condition in Docker Compose / Kubernetes)
- Wrong JDBC URL (typo, wrong port, wrong database name)
- Missing credentials or wrong user/password
- MySQL not accepting connections from the application host (GRANT not set up)
Fix
Step 1 — Verify the connection manually:
mysql -h localhost -P 3306 -u ecommerce_user -p ecommerce
If this fails, the credentials or network are the problem — not Liquibase.
Step 2 — Check the JDBC URL:
spring:
datasource:
url: jdbc:mysql://localhost:3306/ecommerce?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: ecommerce_user
password: secret
driver-class-name: com.mysql.cj.jdbc.Driver
Common URL mistakes:
- Missing
serverTimezone=UTC(causes timezone errors with MySQL 8) - Wrong port (3307 vs 3306)
- Wrong database name
- SSL settings conflicting with MySQL 8 defaults
Step 3 — In Docker Compose, add a health check so the app waits for MySQL:
services:
app:
depends_on:
mysql:
condition: service_healthy
mysql:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 10
Step 4 — In Kubernetes, use an init container:
initContainers:
- name: wait-for-mysql
image: busybox
command:
- sh
- -c
- "until nc -z mysql-service 3306; do echo waiting for mysql; sleep 2; done"
Step 5 — Temporarily disable Liquibase to diagnose the connection issue:
spring:
liquibase:
enabled: false
If the app starts, Liquibase configuration is the problem. If it still fails, the datasource itself is misconfigured.
Quick Reference: Fix Commands
# Release stuck lock
liquibase release-locks
# Clear all checksums (forces recalculation on next run)
liquibase clearCheckSums
# Calculate checksum for a specific changeset
liquibase calculate-checksum \
--changeset-identifier="filepath::id::author"
# Mark a changeset as already ran
liquibase mark-changeset-ran \
--changeset-identifier="filepath::id::author"
# Preview rollback SQL without executing
liquibase future-rollback-sql
# Validate all changesets (checksums, preconditions, syntax)
liquibase validate
# Show pending changesets
liquibase status --verbose
# Show all applied changesets
liquibase history
When None of These Match Your Error
- Run
liquibase validatefirst — it catches the most common issues before attemptingupdate - Add
--log-level=DEBUGto any Liquibase command for verbose output - Check the
DATABASECHANGELOGtable directly — theEXECTYPEandDESCRIPTIONcolumns often tell you exactly what happened - Check the Spring Boot startup log for the full stack trace — the root cause is usually buried under several
caused bylayers
Golden Rules
- Never modify a deployed changeset — checksum errors are the consequence
- Never kill a running migration — stuck locks are the consequence
- Run
liquibase validatein CI — catch problems before they hit production - Write rollback blocks at the same time as changes — they’re impossible to reconstruct later
- Use explicit
includeoverincludeAll— ordering surprises are eliminated entirely