Skip to content
Tech

Zero-Downtime Database Migrations for Laravel Applications

How to change a production schema without locking tables or breaking the currently deployed code, using an expand-and-contract approach.

Zero-Downtime Database Migrations for Laravel Applications
Zero-Downtime Database Migrations for Laravel Applications
In this article

    A migration that runs fine on a development database with a few hundred rows can lock a production table with twenty million rows for long enough to time out every request hitting it. Zero-downtime migration isn't a single trick — it's a discipline of making schema changes in small, backward-compatible steps instead of one big leap.

    The Core Problem: Code and Schema Deploy at Different Times

    During a deploy, there's a window where old application code and a new schema — or new code and an old schema — are both live at once, whether that's seconds or minutes. A migration that renames a column or drops one the old code still reads from will break requests during that window, even if the migration itself runs instantly.

    Expand, Migrate, Contract

    The safe pattern has three phases spread across separate deploys rather than one migration file.

    // Phase 1: Expand — add the new column, nullable, no default backfill yet.
    public function up(): void
    {
        Schema::table('customers', function (Blueprint $table) {
            $table->string('billing_email')->nullable()->after('email');
        });
    }

    Deploy application code that writes to both email and billing_email. Once that's live and the backfill has run, you can read from the new column exclusively:

    UPDATE customers
    SET billing_email = email
    WHERE billing_email IS NULL;

    Only in a later deploy, once nothing reads the old column anymore, do you contract:

    // Phase 3: Contract — safe now that no running code references it.
    public function up(): void
    {
        Schema::table('customers', function (Blueprint $table) {
            $table->dropColumn('email');
        });
    }
    If a migration and its matching code change ship in the same deploy, you haven't eliminated the risky window — you've just made it very short and hoped nobody notices.

    Renaming Is Almost Always Three Steps, Not One

    A direct renameColumn looks tempting but has the same problem as a drop: the instant it runs, any already-running process referencing the old name breaks. Treat a rename as add-new, dual-write, drop-old — the same expand/contract shape as above, just for a rename instead of a removal.

    Watch Table Locks on Large Tables

    MySQL 8 with InnoDB handles most ADD COLUMN operations as online DDL without a full table lock, but it's not universal — adding a column with a non-null default on older configurations, changing a column's type, or adding certain indexes can still take a metadata lock that queues behind long-running queries.

    ALTER TABLE customers
    ADD COLUMN billing_email VARCHAR(255) NULL,
    ALGORITHM=INPLACE, LOCK=NONE;

    Specifying ALGORITHM=INPLACE, LOCK=NONE explicitly makes MySQL fail fast with an error if it can't honor that, rather than silently falling back to a blocking copy of the table — which is exactly the surprise you want to catch in a staging run, not production.

    Tip: Run SHOW PROCESSLIST and check for long-running transactions before kicking off a schema change on a large table — a metadata lock queues behind anything already holding a lock on that table, and a single slow report query can stall your migration indefinitely.

    A Practical Deploy Checklist

    • Migrations that add nullable columns or new tables are generally safe to ship alongside application code.
    • Migrations that drop or rename anything go out after the code that stops depending on it, never before or alongside.
    • Run migrations against a staging database with production-like volume before trusting them on the real thing.
    • Keep each migration file focused on one schema change so a rollback doesn't have to unwind five unrelated things at once.

    None of this is exotic — it's the same discipline teams use for any backward-incompatible API change, applied to the database instead of an endpoint. The schema is just another interface other code depends on, and it deserves the same care.

    Written by Raj

    Founder & Editor

    I write HorizonDesk: practical, no-hype articles on technology, money, business and the vehicles we drive. Everything here is something I’ve researched, tested or wanted explained clearly myself.