PlanetScale — Zero downtime Laravel migrations¶
Holly Guevara, originally 2022-08-29, re-fetched 2026-04-21.
Summary¶
Framework-specific instantiation of PlanetScale's schema-change doctrine for
Laravel applications. The canonical Forge deploy sequence
(git pull → composer install → php artisan migrate) is canonicalised as
the direct-DDL anti-pattern: running artisan migrate against the
production database at deploy time holds a table-level lock for the duration
of an ALTER TABLE, potentially making rows unreadable and unwriteable until
the operation completes. The post's prescription translates PlanetScale's
generic branch-based
workflow into a Laravel-native workflow: run artisan migrate on a
PlanetScale development branch connected to a dev-environment copy of
the app, promote via deploy request, let
PlanetScale's shadow-table
migration handle production rollout without locking. Also canonicalises
Laravel-specific
expand-migrate-contract recipes for
add-column / drop-column / rename-column scenarios, grounded in the
schema-change deploy order
question ("when do I run my migrations?"): schema-before-code for additions,
code-before-schema for drops, multi-step clone-dual-write-backfill-switch for
renames.
Key takeaways¶
-
php artisan migrateat deploy time is the direct-DDL anti-pattern. Verbatim warning: "Runningphp artisan migrateon your production database at deployment can be dangerous, as this can lock your database, preventing reads and writes." MySQL'sALTER TABLE— even the worked example of widening avarchar— may take a table lock "so that the transaction can be completed. This means that nobody will be able to access the table (read or write) while the operation is occurring." The Forge quick-deploy sequence (git pull→composer install→php artisan migrate) is the reference-point anti-pattern. Canonicalises the Laravel-idiomatic instance of the problem PlanetScale's shadow-table pattern solves. (Source: this post.) -
Shadow-table lifecycle, verbatim, as the alternative. Five steps: "Create a copy of the table (known as a shadow table). Apply the schema changes. Get the data in sync between both tables. Swap the tables atomically. Drop the old table." Mechanism is identical to the one canonicalised from [Guevara
-
Noach's schema-reverts internals](<./2026-04-21-planetscale-behind-the-scenes-how-schema-reverts-work.md>) and from the [[sources/2026-04-21-planetscale-the-state-of-online-schema-migrations-in-mysql|2024 state-of-online-schema-migrations survey]]; this post is its Laravel-workflow anchoring. (Source: this post.)
-
PlanetScale blocks production
artisan migrateby default via safe migrations. Verbatim: "if you do try to run migrations on production, it will fail because, in order to protect your production environment, PlanetScale does not support direct DDL on production branches, unless you disable safe migrations. We ultimately leave that decision up to you, but turning off safe migrations means you run the risk of locking tables, which can lead to downtime." Canonical wiki disclosure of safe migrations as a platform-level direct-DDL blocker — the server-side enforcement that converts PlanetScale's architectural prescription into an operational default. (Source: this post.) -
Deploy-order question ("when do I run my migrations?") resolves on schema-change shape. Two baseline cases verbatim: "Example 1: You're adding a field to an input form in your application code, which also requires adding a column to one of your tables. In this case, you have to make sure your schema has been updated in production before that application code goes live. Example 2: You're getting rid of an existing column on one of your tables. In this case, you need to make sure you stop allowing writes to it from the application code before the schema goes live." Canonicalises the schema-first-or-code-first decision as a property of the schema change, not a policy choice. (Source: this post.)
-
Rename (column or table) is a multi-step clone-dual-write-backfill workflow. Eleven-step recipe for renaming a table, verbatim structural sequence: (a) deploy the PlanetScale migration that adds the new table alongside the old one; (b) deploy code that writes to both tables while continuing to read from the old one; (c) backfill old rows into the new table via a script; (d) deploy code that reads from the new table (stopping reads from the old one); (e) deploy the PlanetScale migration that drops the old table. This is the Laravel-specific instantiation of expand-migrate-contract (parallel change) — a rename becomes an add-plus-dual-write-plus-backfill-plus- read-switch-plus-drop sequence. (Source: this post.)
-
30-minute schema-revert window via retained shadow table. Verbatim: "After we swap the original table and the shadow table, instead of just dropping the original table, we actually keep it around for 30 minutes. During those 30 minutes, we continue syncing the two tables. Any changes to the production table data are copied back to the original table." Canonical number: 30 minutes. Mechanism: pre-staged inverse replication keeps the pre-migration table as the pre-staged revert target so a revert is a second swap of an already-in-sync table, not a second data copy. Same mechanism canonicalised from Barnett's 2022-03-24 revert launch and Guevara/Noach's 2022-10 internals; this post is the Laravel-workflow anchoring. (Source: this post.)
-
Revert has unrevertable shapes; the
VARCHARshrink is the canonical example. Verbatim footnote: "theALTER TABLEexample we used earlier where we increase the varchar size is one of these scenarios. If any data was written to the table that was larger than the original varchar size, it won't fit once you revert. In those situations, we will attempt to revert, but if the integrity of your data would be affected we will not proceed." Canonicalises the non-revertible schema change class at the "data-integrity-prevents-rollback" altitude. Reverting a column widening is trivially shape-compatible; reverting a column widening after rows longer than the original max-width have been written is not. PlanetScale's revert path is correctness-preserving rather than best-effort — the revert fails rather than truncates. (Source: this post.)
Systems, concepts, patterns extracted¶
- Systems: systems/laravel, systems/planetscale, systems/mysql, systems/vitess (shadow-table mechanism substrate), Forge (Laravel-official deploy tool, mentioned as the reference anti-pattern sequence; not canonicalised as a wiki system — it's a convenience-deploy product, not a distributed-systems primitive).
- Concepts: concepts/shadow-table, concepts/schema-revert, concepts/online-ddl, concepts/deploy-request, concepts/schema-change-deploy-order, concepts/coupled-vs-decoupled-database-schema-app-deploy, concepts/non-revertible-schema-change, concepts/pre-staged-inverse-replication, concepts/cutover-freeze-point.
- Patterns: patterns/shadow-table-online-schema-change, patterns/expand-migrate-contract, patterns/branch-based-schema-change-workflow, patterns/instant-schema-revert-via-inverse-replication.
Operational numbers¶
- 30 minutes: the schema-revert window, during which the pre-migration table is retained + kept in sync via inverse replication. Reverts initiated within the window are a second atomic swap rather than a data copy.
- 5-step shadow-table lifecycle (create copy → apply schema change → sync → atomic swap → drop old).
- 7-step add-column / drop-column Laravel workflow (dev branch → migration → PlanetScale dev branch → connect → migrate → deploy request → code deploy); deploy-request and code-deploy order flips on schema shape.
- 11-step rename-column / rename-table workflow (clone the column or table, dual-write, backfill, flip reads, drop old — expanded across the code-deploy + schema-deploy axes).
Caveats¶
- Laravel-specific framing; the architectural substrate (shadow table, deploy request, 30-minute revert window, safe migrations) is identical to the framework-agnostic corpus canonicalised across Burns 2021, Guevara/Noach 2022, Barnett 2022, and Noach 2022 — this post's contribution is the translation into Laravel deploy conventions, not the mechanism.
- Pedagogy voice; no production numbers from PlanetScale fleet (p95 lock duration on direct DDL, % of customers hit by safe-migrations blocker, revert-invocation rate, etc.).
- Forge-centric deploy-sequence framing; Laravel users on Envoyer, Deployer, or Kubernetes-based setups have equivalent anti-patterns but the post doesn't generalise.
- No engagement with Laravel's
schema:dump+migrate:freshworkflows, withDB::unprepared()raw DDL inside migrations, with multi-databaseconnectionattributes, or with Laravel queue-worker schema assumptions under migration in flight. - The rename recipe is correct but cost-underspecified: the backfill step can be multi-hour on large tables, the dual-write period can be days-to-weeks in practice, and the final drop is itself a schema change subject to its own deploy-request + shadow-table + revert-window rules.
- 30-minute window is a product-policy constant, not a mechanism constraint; sibling posts disclose PlanetScale adjusts it over time.
- No SLO / MTTR figures on Vitess cutover; "atomic" swap is sub-second-class but the exact distribution is not disclosed.
- Safe-migrations can be disabled per-branch; post names the tradeoff ("leave that decision up to you") but does not enumerate the legitimate cases (emergency hotfixes, narrow test environments) where disabling is appropriate.
Source¶
- Original: https://planetscale.com/blog/zero-downtime-laravel-migrations
- Raw markdown:
raw/planetscale/2026-04-21-zero-downtime-laravel-migrations-0380496f.md
Related¶
- systems/laravel — framework the post is written for
- systems/planetscale — platform whose schema-change doctrine the post instantiates
- concepts/shadow-table — mechanism substrate
- concepts/schema-revert — 30-minute post-cutover revert window
- concepts/schema-change-deploy-order — "when do I run my migrations?" decision axis
- patterns/expand-migrate-contract — rename recipe is an instance
- patterns/shadow-table-online-schema-change — the mechanism in the post
- patterns/branch-based-schema-change-workflow — PlanetScale-generic workflow this post translates to Laravel
- sources/2026-04-21-planetscale-serverless-laravel-applications-with-aws-lambda-and-planetscale — sibling Laravel-on-PlanetScale post (runtime axis vs this one's schema-change axis)
- sources/2026-04-21-planetscale-non-blocking-schema-changes — 2021 launch post for the branch + deploy-request substrate
- sources/2026-04-21-planetscale-behind-the-scenes-how-schema-reverts-work — mechanism internals for the 30-minute window
- sources/2026-04-21-planetscale-revert-a-migration-without-losing-data — 2022 revert launch post
- sources/2026-04-21-planetscale-the-operational-relational-schema-paradigm — Noach's ten-tenet manifesto; this post is a Laravel-workflow instance of tenets 1, 2, 5, 7, 8
- companies/planetscale — company page