Skip to content
EdgeServers
Blog

The Laravel migrations that break production — and the safe patterns we use instead

May 27, 2026 · 1 min read · by Sudhanshu K.

There's a category of Laravel migrations that always works on staging and frequently breaks production: rename a column, drop a column, change a type, add a NOT NULL on a populated table. The pattern is the same in every case — the schema change requires a full table lock or a deploy-coordinated cutover, and Laravel's defaults give you neither.

This is the migration-safety discipline we apply to every managed Laravel customer with a non-trivial database.

The safe two-step rename

// Migration 1 — deploy
Schema::table('users', function (Blueprint $t) {
    $t->string('email_address')->nullable();   // add the new column
});
// Backfill via a queue job, not in the migration
 
// Migration 2 — deployed only after the backfill completes
Schema::table('users', function (Blueprint $t) {
    $t->string('email_address')->nullable(false)->change();
    $t->dropColumn('email');                   // drop the old column
});

The rule: every "rename" or "type change" becomes two deploys with a backfill in between. The first deploy adds the new column. The application code learns to read and write both. A background job backfills. After the backfill is complete and the application is no longer reading the old column, deploy two removes it.

The full write-up covers:

  • The "works on small staging table" trap (table locks scale with row count)
  • --pretend discipline — read the actual SQL before running it
  • Online schema change tools (gh-ost, pt-online-schema-change) for the cases where Laravel migrations aren't enough
  • Backfilling via queue jobs, not via migrations
  • The migration template we standardize for customers
  • Rollback strategy when a migration has already run and the deploy fails

Reach out if your last migration window was longer than it should have been.

Full article available

Read the full article