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)
--pretenddiscipline — 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