Migration Tool Comparison

The migration tool you adopt is a long-term operational contract, not a syntax preference. Each runner makes three decisions on your behalf that determine whether a deploy is safe: how it detects that an already-applied migration has been edited (its checksum model), how it records which versions ran (its schema-history table), and how it stops two runners from racing the same database (its locking strategy). Get these wrong and you discover the mismatch at 3 a.m., when a hand-edited migration fails its checksum on production while a concurrent deploy holds a stale advisory lock. This page serves backend and platform engineers selecting a runner, and DBAs who must guarantee the history table and lock behavior survive every release.

Tool choice sits at the root of the broader Database Migration Fundamentals discipline: the runner enforces ordering and immutability so that Schema Version Control Basics hold under merge pressure, and it must respect the transactional versus non-transactional split between PostgreSQL and MySQL or it will leave a database half-migrated on failure. This guide compares Flyway, Liquibase, Alembic, and Prisma Migrate on exactly those mechanisms — not on which has the nicer CLI.

How a migration runner decides to apply a change A migration file passes through three runner mechanisms in order — checksum verification against the history table, version-ordering lookup, and lock acquisition — before the DDL is applied; any check failing aborts the run. Three Gates Inside the Runner 1. Checksum Hash file vs history record 2. History Resolve order, skip applied 3. Lock Acquire single writer lock Apply DDL Record version release lock any gate fails → abort, leave history untouched Tools differ in how strict each gate is and what it does on failure.
Every runner implements these three gates; the comparison is in how strict each gate is and whether a failed gate aborts cleanly.

Concept & Mechanism

A migration runner is a small state machine wrapped around your DDL. Understanding the comparison means understanding the three mechanisms it implements, because that is where the tools genuinely diverge.

Checksum validation. Once a migration is applied, the runner stores a hash of the file alongside its version number. On the next run it re-hashes the on-disk file and compares. If they differ — because someone edited an applied migration — strict tools refuse to run. Flyway computes a CRC32 over the SQL file and aborts with Migration checksum mismatch. Liquibase stores an MD5 in the MD5SUM column of DATABASECHANGELOG and likewise refuses. Alembic does not checksum migration bodies at all — it trusts the revision graph — so an edited migration that already ran is silently ignored. Prisma records a checksum in _prisma_migrations and treats a mismatch as drift, prompting a reset in development and failing migrate deploy in production.

Schema-history tracking. The runner needs a durable record of what ran, in what order. The table name and ordering model differ sharply. Flyway uses flyway_schema_history with monotonic version strings (V1, V1.1, V2). Liquibase uses DATABASECHANGELOG, keyed by the changeset’s id/author/filename triple rather than a single version, which is why Liquibase tolerates out-of-order authoring better. Alembic stores a single version_num row in alembic_version and resolves order through a down_revision pointer graph, not a sortable string. Prisma writes one row per migration directory into _prisma_migrations, ordered by directory timestamp prefix.

Lock behavior. To stop two deploys racing the same database, each runner acquires a lock. Flyway and Liquibase take a database-level lock — Flyway via a session-level advisory lock (a pg_advisory_lock on PostgreSQL), Liquibase via a row it inserts into a DATABASECHANGELOGLOCK table. The Liquibase table-row lock is the dangerous one: if a runner is killed mid-migration, the lock row is never released and every subsequent run hangs until you manually clear it. Alembic and Prisma take a transaction-scoped lock only where the engine supports transactional DDL, which on MySQL means effectively no cross-statement lock — a critical interaction with the transactional versus non-transactional behavior covered separately.

What each runner's lock does when the runner is killed mid-migration Flyway holds a session-level advisory lock that the database frees automatically when the connection dies. Liquibase inserts a row into DATABASECHANGELOGLOCK that survives the crash and hangs every future run until releaseLocks is run by hand. Alembic and Prisma take only a transaction-scoped lock, which the rolled-back transaction releases on crash. Lock Models Under a Killed Runner Runner Lock mechanism On mid-migration kill Flyway Session advisory lock (pg_advisory_lock) Auto-frees on disconnect Liquibase Row in DATABASECHANGELOGLOCK Row survives — every run hangs until manual releaseLocks Alembic Transaction-scoped only (where DDL is transactional) Rollback releases lock Prisma Transaction-scoped only (no cross-statement lock on MySQL) Rollback releases lock
Only Liquibase's table-row lock outlives the process that took it — a killed runner leaves the row set, and every later run blocks until an operator runs releaseLocks.

Prerequisites & Decision Criteria

Pick the runner against your operational constraints, not its feature list. Use this checklist to score candidates before committing:

Decision axis Flyway Liquibase Alembic Prisma Migrate
Checksum model CRC32 per file, hard-fail MD5 per changeset, hard-fail None (revision graph only) Per-migration hash, drift error
History table flyway_schema_history DATABASECHANGELOG alembic_version _prisma_migrations
Ordering Version string sort id/author/file triple down_revision graph Directory timestamp prefix
Lock mechanism Session advisory lock Row in DATABASECHANGELOGLOCK Transaction-scoped only Transaction-scoped only
Crash-stuck lock risk Low (session lock auto-frees) High (manual releaseLocks needed) Low Low

If your team forbids editing applied migrations and runs primarily PostgreSQL, Flyway’s session advisory lock and CRC32 hard-fail are the safest defaults — the deeper trade-offs are in Flyway vs Liquibase: Choosing the Right Migration Tool.

Step-by-Step Procedure

Evaluate candidates against a fresh, production-like database rather than trusting documentation. Each step produces evidence you can attach to an architecture decision record.

1. Provision an ephemeral database that mirrors production. Match the engine major version and privilege model from your environment parity baseline so lock and DDL behavior is representative.

# Context: throwaway CI database; full DDL privileges; never point this at production.
# PostgreSQL 16 to match the prod LTS line.
docker run -d --name tool-eval -e POSTGRES_PASSWORD=eval -p 5432:5432 postgres:16

Verify before proceeding: psql -h localhost -U postgres -c '\l' lists the database and you can create a table.

2. Apply a representative migration and capture the history table. Use a real additive change, not a toy one.

-- PostgreSQL · run as the migration role · safe on an empty eval DB, NOT production.
-- After the runner applies this, inspect its history table.
ALTER TABLE orders ADD COLUMN IF NOT EXISTS region_code VARCHAR(8);
-- Then, for Flyway:
SELECT version, checksum, success FROM flyway_schema_history ORDER BY installed_rank;

Verify before proceeding: exactly one new history row exists with success = true and a non-null checksum.

3. Force a checksum mismatch. Edit the already-applied migration file and re-run the runner. Record the exact error and exit code.

# Context: deliberately corrupting an applied migration on the EVAL db to test strictness.
echo "-- harmless trailing comment" >> migrations/V2__add_region.sql
flyway migrate   # expect: "Migration checksum mismatch for migration version 2"
echo $?          # expect non-zero

Verify before proceeding: Flyway and Liquibase abort; note whether Alembic silently proceeds, which tells you it will not protect you here.

4. Simulate a crashed runner mid-lock. Start a migration and kill the process, then start a second runner. This reveals the stuck-lock failure mode.

# Context: EVAL db only. Kill the runner while it holds the lock, then retry.
liquibase update & sleep 1 && kill -9 $!
liquibase update   # Liquibase: hangs / "Could not acquire change log lock"

Verify before proceeding: confirm whether the second run blocks, and document the unlock command (liquibase releaseLocks) in your runbook.

Verification & Observability

Trust the database’s own state, not the runner’s exit message. These queries confirm the history table and locks are clean before you promote a tool or a deploy.

-- PostgreSQL · read-only · safe on production with a read role.
-- Confirm no failed migration was left half-recorded (Flyway).
SELECT version, success FROM flyway_schema_history WHERE success = false;

-- Confirm no advisory lock is still held by a dead session.
SELECT pid, locktype, objid, granted
FROM pg_locks WHERE locktype = 'advisory';
-- PostgreSQL/MySQL · read-only · run against the live DB during a deploy.
-- Liquibase: detect a stuck lock row that will block every future run.
SELECT id, locked, lockedby, lockgranted FROM databasechangeloglock WHERE locked = TRUE;

For MySQL, inspect lock contention directly while a migration runs, since the runner cannot wrap statements in one transaction:

-- MySQL 8.0 · read-only · run from a second session during the migration.
-- Look for the migration thread holding a metadata lock on the target table.
SELECT * FROM performance_schema.metadata_locks WHERE OBJECT_NAME = 'orders';
SHOW ENGINE INNODB STATUS\G

Rollback Path

Rolling back a tool evaluation is cheap; rolling back a tool migration in production is not. The safe path depends on what you changed.

If a candidate failed its checks, simply drop the ephemeral database and record the failure in the ADR — no production state was touched. If you applied a migration through a runner on a real database and must reverse it, do not delete history rows by hand. Run the runner’s own reverse if it has one (Flyway undo requires the paid edition; Liquibase rollback uses the changeset’s declared reverse), and only then verify the history table reflects the reversal:

-- PostgreSQL · migration role · safe only after the inverse DDL has committed.
-- Confirm the history no longer lists the reversed version as applied.
SELECT version, success FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 5;

Safe conditions for any rollback: the inverse DDL is non-destructive (it does not DROP a column the previous app version still reads), the runner’s lock is released, and no concurrent deploy is in flight. If those do not hold, halt and restore from a pre-migration snapshot instead.

Common Errors & Fixes

Migration checksum mismatch for migration version N (Flyway) / Validation Failed: changeset checksum changed (Liquibase). Root cause: an already-applied migration file was edited. Fix: never edit applied migrations — author a new forward migration. If the edit was cosmetic and intentional, run flyway repair (or Liquibase clearCheckSums) to re-baseline the stored hash, but only after confirming the SQL body did not change behavior.

Could not acquire change log lock. Currently locked by <host> (Liquibase). Root cause: a previous runner was killed before releasing its DATABASECHANGELOGLOCK row. Fix: confirm no migration is actually running, then liquibase releaseLocks. Add a deploy-timeout so a hung runner cannot hold the lock indefinitely.

Target database is not up to date (Alembic). Root cause: the alembic_version row points to a revision that is not the head of your down_revision graph, usually after a bad merge. Fix: run alembic heads to find divergence, then author a merge revision — this is the conflict-resolution pattern that Schema Version Control Basics addresses for every runner.

The migration ... was modified after it was applied (Prisma). Root cause: a file inside an already-deployed migrations/ directory changed, so its recorded checksum no longer matches. Fix: in production, never edit applied directories; generate a new migration. Resolve the recorded state with prisma migrate resolve only when you are certain the database already reflects the intended schema.

Routing a runner error to its root cause and fix Four error signatures each map to a root cause and a fix: a checksum mismatch means an applied file was edited, fixed by authoring a new forward migration; a change-log lock error means a killed runner left the lock, fixed by releaseLocks after confirming no run is active; Alembic not up to date means the version pointer left the head, fixed by a merge revision; Prisma modified-after-applied means a deployed directory changed, fixed by migrate resolve. Error Signature to Fix Error seen Root cause Fix checksum mismatch (Flyway / Liquibase) An applied file was edited Author a new forward migration Could not acquire change log lock Killed runner left the lock row set releaseLocks after confirming no active run not up to date (Alembic) Version pointer is off the graph head Author a merge revision modified after applied (Prisma) A deployed directory changed on disk prisma migrate resolve once schema matches Never hand-delete a history row — always route through the runner's own fix.
Every one of these signatures traces back to editing or losing history state; the fix is always a new forward action or the runner's own repair command, never a manual row delete.

Matching the Tool to Your Failure Modes

The comparison that matters is not feature-count but which of your failure modes a runner makes cheap to prevent. If your recurring pain is someone editing an already-applied migration and silently diverging environments, weight checksum verification heavily and accept a more rigid workflow to get it. If your pain is migrations that lock production unexpectedly, weight lock visibility and the ability to run a statement-level dry run that surfaces the lock before the deploy. If you run MySQL and have been burned by half-applied migrations, weight honest handling of implicit commits over a tool that pretends every migration is atomic. A runner that scores well on a feature matrix but obscures the one property your team keeps tripping over is the wrong tool regardless of the matrix. Choose for the guarantee you most need to stop paying for by hand, and treat everything else as secondary.

Child Page Index

This section drills into the choices the comparison surfaces. Flyway vs Liquibase: Choosing the Right Migration Tool compares the two most common SQL-centric runners head to head on checksum strictness, the table-row versus advisory lock trade-off, and multi-database syntax handling. A step before that, Versioned vs State-Based Migrations reframes the whole comparison around a more basic fork — whether you author imperative version scripts (Flyway, Alembic) or declare a desired end-state and let a tool diff it (Atlas, Skeema) — and how each model handles drift, review, and rollback. When you have already standardized on one tool and need to switch without an outage, Migrating from Flyway to Liquibase Without Downtime walks through reconciling the two history tables, preserving applied-version state, and running both runners in parallel during the cutover. All three build on the broader Database Migration Fundamentals that define what a correct, ordered, immutable migration is in the first place.

Frequently Asked Questions

Does the migration tool I pick affect whether a deploy can be zero-downtime? Indirectly but significantly. The tool decides lock behavior and whether a failed step rolls back cleanly. A runner that holds a long advisory lock, or one that leaves a MySQL migration half-applied because it assumed transactional DDL, can turn a routine additive change into an outage regardless of how careful the SQL is.

Why does Alembic let me edit an applied migration when Flyway refuses? Alembic does not checksum migration bodies; it trusts the revision graph defined by down_revision pointers. That makes branching and merging flexible but removes a safety net — an edited migration that already ran is silently ignored. If your team needs hard protection against tampering, prefer a CRC/MD5-checksumming runner like Flyway or Liquibase.

What is the single most dangerous difference between these tools in production? The lock model. Liquibase’s lock is a row in DATABASECHANGELOGLOCK; if a runner is killed mid-migration, that row is never cleared and every future run hangs until you manually run releaseLocks. Flyway’s session-level advisory lock frees automatically when the connection dies, which is why it fails safer under crash conditions.