{"post":{"slug":"database-migration-strategies-2026","title":"Database Migration Strategies for Production (2026)","excerpt":"Schema changes fail in production for the same handful of reasons every time: locks, incompatible deploy ordering, and no rollback path. Here are the migration strategies that avoid all three in 2026.","category":"code","date":"July 31, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">S</span>chema migrations fail in production for a short list of recurring reasons: a lock held too long on a hot table, a schema change deployed in lockstep with code that assumes it already happened, or no tested way back when something goes wrong. None of these are solved by picking a fancier migration tool — they are solved by picking the right strategy for the change and the table size involved. This guide covers the strategies themselves: when a straightforward blocking change is fine, when you need expand-contract, and when only an online schema change tool will do.\n\n## What changed in 2026\n\n- **Online schema change tools became routine, not exotic.** gh-ost, `pt-online-schema-change`, and Postgres-native pgroll are now common defaults for any migration touching a table over a few million rows, not a last resort.\n- **Branch-based database testing spread widely.** Neon and PlanetScale branching let teams run a migration against a full copy of production data before it ever touches the real database.\n- **Declarative schema tooling grew alongside imperative migrations.** Tools that diff a desired schema state against the live database complement, rather than replace, hand-written migration steps for anything involving data backfills.\n- **Migration-aware deploy pipelines matured.** More CI/CD systems now enforce that a migration runs and is verified as a distinct pipeline stage before the new application version receives traffic.\n\n## The four migration strategies\n\n| Strategy | How it works | Use when |\n| --- | --- | --- |\n| Big-bang (blocking) | Single `ALTER` runs directly against the live table | Table is small, or the change is genuinely instant (e.g., adding a nullable column on Postgres 11+) |\n| Expand-contract | Add new structure, dual-write or backfill, cut over reads, then remove the old structure — across several deploys | Any change that isn't backward compatible in one step: renames, type changes, NOT NULL additions |\n| Online schema change tool | A copy table is created, rows are copied in batches with change tracking, then an atomic rename swaps it in | Large or high-traffic tables where even a brief lock is unacceptable |\n| Dual-write with backfill | Application writes to both old and new locations while a background job backfills history | Splitting a table, changing a primary data store, or introducing a new service of record |\n\nMost production incidents come from applying the big-bang strategy to a change that actually needed expand-contract — the migration itself may run in milliseconds, but the application code deployed alongside it assumes a state that other, still-running instances haven't reached yet.\n\n## Migration ordering across a rolling deploy\n\nDuring a rolling deploy, old and new application code run against the same database at the same time, often for minutes. That means every migration must be compatible with both:\n\n```\nT0: migration runs — adds new column, nullable, no default\nT1: rolling deploy begins — some instances run old code, some run new\nT2: new code writes to the new column; old code ignores it (safe)\nT3: rolling deploy completes — all instances run new code\nT4: next deploy: backfill + set NOT NULL, now that old code is gone\n```\n\nReversing steps 1 and 3 — deploying code that requires the new column before the migration that creates it has finished — is the single most common cause of migration-related outages. The rule: **the schema change ships first and alone; the code that depends on it ships in a later, separate deploy.**\n\n## Common mistakes\n\n**Running a schema change and its dependent code in the same deploy.** Even if the migration finishes in milliseconds, a rolling deploy means some instances are still on old code when new code starts querying a column or table that isn't guaranteed to exist yet on every replica the old code reads from.\n\n**Adding a `NOT NULL` column with no default directly.** On most databases this requires validating every existing row under a lock. Add it nullable, backfill in batches, then add the constraint once the backfill is verified complete.\n\n**Assuming a small migration on a small table today stays small.** A table with a few thousand rows can grow past the point where a blocking `ALTER` is safe within a year; revisit the strategy, not just the migration file, as tables grow.\n\n**No tested rollback for a destructive change.** A `DROP COLUMN` or a data-changing `UPDATE` needs a verified restoration path — a backup, a shadow copy, or a reversible expand-contract step — before it runs anywhere near production.\n\n## FAQ\n\n**Do online schema change tools work with any database?**\nMostly by ecosystem — gh-ost and `pt-online-schema-change` target MySQL, pgroll targets Postgres. Each works by copying data into a shadow table and swapping it in, rather than altering the live table directly.\n\n**How long should expand-contract take?**\nAs long as it takes to safely roll out each stage — often days, sometimes longer for high-risk changes, since each stage is typically its own deploy with its own monitoring window before moving to the next.\n\n**Can I combine expand-contract with a feature flag?**\nYes, and it is a common, safe pattern — gate the code that reads from the new structure behind a flag, verify it against production traffic at low exposure, then ramp up before removing the old structure entirely.\n\n**What is the safest way to test a risky migration before running it in production?**\nRun it against a full, recent copy of production data — a database branch (Neon, PlanetScale) or a restored snapshot — and time it, check for lock waits, and verify the resulting data, not just that the migration command exits successfully.\n\n## Where to go next\n\nSee [zero-downtime deployment in 2026](/blog/zero-downtime-deployment-guide-2026) for coordinating schema changes with the application deploys that depend on them, [database sharding explained for 2026](/blog/database-sharding-explained-2026) for migrations that span multiple database nodes, and [how to pick a database in 2026](/blog/how-to-pick-a-database-2026) if the migration you're avoiding is actually a sign you picked the wrong data store.","hue":320,"metaTitle":"Database Migration Strategies for 2026","metaDescription":"Database migration strategies for 2026: expand-contract, online schema change tools, deploy-ordering rules, and how to avoid locking production.","keywords":["database migration strategies 2026","safe schema migration production","expand contract migration pattern","online schema change tools","zero downtime schema change","gh-ost pt-online-schema-change","migration deploy ordering","backward compatible database migration","rolling deploy schema change"],"tldr":["**Pick a strategy before you pick a tool** — big-bang, expand-contract, and online schema change each solve a different risk, and the tool you use just executes the strategy.","**A migration must be compatible with two app versions at once** — during a rolling deploy, old and new code both query the same schema simultaneously.","**Locking, not data volume, is usually what causes an incident** — a multi-second table lock on a hot table matters more than a migration that simply takes a while to run.","**Online schema change tools rewrite big tables without long locks** — gh-ost, pt-online-schema-change, and pgroll trade extra disk and time for avoiding a blocking `ALTER TABLE`.","**Skip:** shipping a schema change and the code that depends on it in the same deploy — that is what turns a routine migration into an outage during a rolling restart."],"readTime":"5 min read","wordCount":1001},"related":[{"slug":"expand-contract-migrations-2026","title":"Expand and Contract: Schema Changes Without Downtime","excerpt":"Renaming a column in one deploy breaks every running instance of the old code. Splitting the change into three deploys makes it invisible to users.","category":"code","date":"August 13, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":320},{"slug":"how-to-write-a-database-migration-2026","title":"How to write a database migration in 2026: safe, reversible, and zero-downtime","excerpt":"Database migrations fail when they lock tables in production or cannot be rolled back. This guide covers the 2026 patterns for safe, zero-downtime schema changes in Postgres.","category":"code","date":"April 6, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":305},{"slug":"what-is-a-database-migration-2026","title":"What is a database migration in 2026? Schema changes, versioned","excerpt":"A database migration is a versioned, repeatable change to a schema, tracked the same way source code is. Here is what that means in practice, and why teams stopped editing production schemas by hand in 2026.","category":"code","date":"July 17, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":150}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"data-pipeline-orchestration-tools-2026","title":"Data Pipeline Orchestration Tools Compared for 2026","excerpt":"Airflow, Dagster, Prefect, and Temporal all schedule and run data pipelines, but they fit different workloads. Here is how to pick the right orchestrator in 2026.","category":"code","date":"July 31, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":308},"older":{"slug":"database-sharding-explained-2026","title":"Database Sharding Explained: Scaling Writes in 2026","excerpt":"A single primary can only absorb so many writes before disk I/O and lock contention cap throughput. Sharding splits data across independent primaries so write capacity scales horizontally instead of hitting a wall.","category":"code","date":"July 31, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":312}}