Flyway vs Liquibase: Choosing the Right Migration Tool
You are about to standardize a team on one migration tool, and the decision is not about XML versus SQL — it is about which tool’s failure modes you are willing to operate. Flyway and Liquibase diverge most where it costs you most: how each handles a transaction boundary on a failed migration, how each tracks checksums, and how each recovers from a locked history table after a CI/CD run dies. Pick wrong and you inherit either silent half-applied schemas or noisy checksum drift on every branch merge. This page compares the two on the axes that decide a zero-downtime deploy, and sits within the broader migration tool comparison section of the migration fundamentals guide.
Symptom / Error Signatures
These signatures point to a tool-behavior mismatch rather than a bad script:
ERROR: lock timeout/deadlock detectedduringALTER TABLEfrom a migration runnerFlywayException: Migration checksum mismatchorLiquibaseException: Checksum validation failedflyway_schema_historyorDATABASECHANGELOGheld by an orphaned CI/CD session after a runner crash- PostgreSQL
ERROR: cannot execute ALTER TABLE in a read-only transaction - Connection-pool exhaustion as a prolonged DDL blocks concurrent reads and writes
Root Cause Analysis
The divergence is rooted in how each tool manages the transaction boundary and the history table. Flyway Community does not wrap each migration in a transaction by default on most databases — a failed migration leaves the database partially altered unless you wrap the SQL yourself in BEGIN/COMMIT. Liquibase wraps each changeset in a transaction where the database supports it, at the cost of XML/YAML parsing and pre-flight validation overhead. On top of that, both tools track applied state in a history table, and concurrent pipelines competing for that table produce the lock contention you see in the logs. Both also surface checksum errors when legacy scripts lack conditional guards, which is why idempotent script design reduces churn regardless of tool. Crucially, the transactional advantage evaporates on MySQL, where DDL forces an implicit commit — the reason is detailed in handling non-transactional DDL in MySQL migrations.
| Dimension | Flyway (Community) | Liquibase (Open Source) |
|---|---|---|
| Migration format | Versioned SQL files | SQL, XML, YAML, JSON changesets |
| Rollback support | Manual U scripts; undo requires Teams/Enterprise |
liquibase rollback <tag> built-in |
| Checksum enforcement | Per-file CRC-32; repair clears failed entries |
Per-changeset MD5; clearCheckSums forces recalc |
| Transactional wrapping | Manual (BEGIN/COMMIT in your SQL) |
Per-changeset, configurable via runInTransaction |
| CI/CD integration | Docker image, Maven/Gradle plugins, CLI | Same, plus Spring Boot auto-configuration |
Immediate Mitigation
Run as a privileged DBA or service account with pg_terminate_backend / MySQL PROCESS privilege. Halt all deployment pipelines first, and avoid peak traffic windows.
- Diagnose the blocking session holding the history table:
-- PostgreSQL · read-only diagnostic · run as a role that can see all sessions
SELECT pid, usename, state, query, wait_event_type, wait_event
FROM pg_stat_activity
WHERE (query LIKE '%flyway_schema_history%' OR query LIKE '%DATABASECHANGELOG%')
AND state = 'active'
AND pid <> pg_backend_pid();
-- MySQL · read-only diagnostic · run as a privileged user
SELECT * FROM information_schema.innodb_trx
WHERE trx_query LIKE '%DATABASECHANGELOG%';
- Terminate the orphaned lock — PostgreSQL
SELECT pg_terminate_backend(<pid>);, MySQLKILL <trx_mysql_thread_id>;. Confirm the session is orphaned, not actively committing. - Reconcile the history table. Flyway:
flyway repairfixes checksums of failed migrations and removes failed entries. Liquibase:liquibase clearCheckSumsforces recalculation on the next run. - Reverse explicitly when needed. Flyway Teams/Enterprise:
flyway undo -target=<previous_version>. Flyway Community: run pre-written inverse DDL wrapped inBEGIN; ... COMMIT;. Liquibase:liquibase rollback <tag_or_date>.
During reconciliation, route application traffic to a read replica or trip circuit breakers. Never force-reset a history table without a verified logical backup.
Permanent Fix / Long-Term Pattern
Remove lock contention and checksum drift with architectural controls rather than tool tuning. Decouple schema from app deploys with expand-and-contract: add columns and tables first, ship the code, drop legacy objects in a later release. Make transaction handling explicit — on Flyway, wrap DDL in BEGIN;/COMMIT; in your migration files (and remember PostgreSQL DDL is transactional while MySQL DDL is not); on Liquibase, set runInTransaction="false" for statements that cannot run inside a transaction, such as CREATE INDEX CONCURRENTLY. Enforce idempotent script design with IF NOT EXISTS guards and online-build flags (CREATE INDEX CONCURRENTLY on PostgreSQL, ALGORITHM=INPLACE, LOCK=NONE on MySQL). Add a pre-flight gate that runs flyway info or liquibase updateSQL against a staging replica before promotion, and enforce a single-runner policy so no two pipelines target the same history table at once. If a switch is on the table, plan it with migrating from Flyway to Liquibase without downtime.
Verification Checklist
BEGIN/COMMITon Flyway,runInTransactionon Liquibase)flyway validate/liquibase validatereports zero checksum mismatches across all environmentsflyway_schema_historyorDATABASECHANGELOG(verified viapg_stat_activity/innodb_trx)CREATE INDEX CONCURRENTLY/ALGORITHM=INPLACE, LOCK=NONE) are present on every index migration
Frequently Asked Questions
Which tool should I pick if rollback matters most?
Liquibase, because liquibase rollback <tag> is a first-class, built-in primitive in the open-source edition, whereas Flyway’s undo requires a paid tier and otherwise relies on hand-written U scripts. That said, on MySQL neither tool can truly roll back DDL — the engine commits it implicitly — so for MySQL the rollback story is your inverse-DDL discipline, not the tool.
Does Flyway really leave the database half-migrated on failure?
On most databases the Community edition does not wrap a migration in a transaction by default, so a multi-statement script that fails partway leaves earlier statements applied. The fix is to wrap your DDL in explicit BEGIN;/COMMIT; (effective on PostgreSQL) and to keep every script idempotent so a retry converges.
How do I stop checksum mismatches on every branch merge?
The mismatch usually means a migration file changed after it was applied. Treat applied migrations as immutable, add new files instead of editing old ones, and reserve flyway repair / liquibase clearCheckSums for genuine reconciliation rather than as a routine step in the pipeline.