Django Migration Review Checklist

A code-review checklist for Django migrations before they reach production: backward compatibility with running code, locking and table rewrites, large-table operations, data migrations, reversibility, dependencies, and deployment order.

Problem statement

Most Django production outages caused by deploys trace back to a migration: a column dropped while old code still reads it, an index build that locked a busy table for minutes, a data migration that loaded a million rows into memory, or a NOT NULL column added without a default on a large table. Migrations look small in a diff, so reviewers skim them. This checklist gives reviewers specific questions to ask of every migration file before it merges.

Reviewing a migrationCompatibilityold code still works?Locksrewrites, long holds?Databatched, idempotent?Orderdeploy sequence clear?Ask these four questions of every migration before it reaches production.
  1. Compatibility: can the currently running code work with the new schema during the deploy?
  2. Locks: does any operation rewrite or exclusively lock a large table? Use sqlmigrate to see the SQL.
  3. Indexes on large tables use AddIndexConcurrently in a non-atomic migration.
  4. Data migrations are separate, batched, idempotent, and use historical models (apps.get_model).
  5. Destructive changes (drop column or table) happen in a later release, after code stops using them.
  6. Reversibility and dependencies are intentional and documented.

Checklist

Backward compatibility

  • New columns are nullable or have a database default, so old code inserting rows without them still works.
  • No column or table is removed or renamed in the same release as the code change that stops using it (expand, then contract).
  • Renames are done as add-copy-switch-drop across releases, not RenameField on hot tables during traffic.
  • Model changes that alter querysets (new default ordering, managers) are checked for old-code impact.

Locks and table rewrites

  • python manage.py sqlmigrate app 00NN reviewed for each migration.
  • No ALTER COLUMN TYPE that rewrites a large table without a plan (new column + backfill instead).
  • NOT NULL added to existing large columns via a CHECK constraint NOT VALID, validated separately, where needed.
  • lock_timeout set for migrations on busy tables so they fail fast instead of queueing traffic behind them.
operation to safer patternRisky operationSafer patternRemoveField with old code livestop using it first, drop next releaseAddIndex on a large tableAddIndexConcurrently, atomic = FalseAlterField type changenew column, backfill, switch, dropAddField NOT NULL, no defaultnullable or db_default, then enforceRunPython over all rowsbatched, separate migration or commandMost risky operations have an expand-and-contract equivalentdjango-deployment.com
Ask for the safer pattern in review when the table is large or busy.

Data migrations

  • Data changes live in their own migration, not mixed with schema operations.
  • Code uses apps.get_model(), not direct model imports, so it works with historical schemas.
  • Large updates run in batches (iterator, update by primary key ranges) or as a management command outside the migration.
  • The operation is idempotent: rerunning it does no harm.
  • A reverse_code is provided, or migrations.RunPython.noop with a comment explaining why reversing is not needed.

Structure and dependencies

  • Migration names are descriptive; squashing is planned for long histories.
  • Dependencies across apps are correct and do not create cycles; no conflicting leaf nodes.
  • Non-atomic migrations (atomic = False) contain only the operations that require it.
  • Third-party app migrations pulled in by a package upgrade are reviewed too.

Deployment plan

  • The PR states whether migrations must run before or after code, and if a follow-up release is needed.
  • Estimated runtime on production data is known (tested on a production-sized copy for big tables).
  • A rollback plan exists: previous code works with the new schema.
approve a migration whenOld code works with the new schemaexpand before contractSQL reviewed, no long lockssqlmigrate, lock_timeoutBig indexes built concurrentlynon-atomic migrationData changes batched and idempotenthistorical modelsDeploy order and runtime statedrollback possibleFive questions catch most migration-caused outages before mergedjango-deployment.com
Add these questions to your pull request template for migration files.

Explanation

During any deploy, old and new code overlap with one schema: migrations run while old processes still serve requests, and rollbacks put old code on the new schema. Migrations must therefore be backward compatible, which the expand-and-contract pattern guarantees by splitting destructive changes across releases. Separately, PostgreSQL takes locks for schema changes; operations that rewrite tables or wait behind long transactions can block all traffic. Reviewing the generated SQL and choosing concurrent or incremental alternatives on large tables prevents those outages.

Edge cases / notes

  • MySQL commits DDL immediately; a failing migration can leave partial changes, so review is even more important.
  • Squashed migrations must be applied everywhere before removing the originals.
  • Tools such as django-migration-linter and django-pg-zero-downtime-migrations automate parts of this review.
  • Small tables rarely need special treatment; focus effort on tables with many rows or heavy write traffic.

Next steps

FAQ

What should I check when reviewing a Django migration?

Backward compatibility with running code, locks and rewrites in the generated SQL, index builds, data migration batching, and deploy order.

How do I see the SQL a migration will run?

python manage.py sqlmigrate <app> <migration_number>.

Is removing a field in a migration safe?

Only after the code that uses it has been deployed without it; drop the column in a later release.

Should data migrations be in the same file as schema changes?

No. Keep them separate so they can be batched, reviewed, and rerun independently.

Learning path · step 4 of 14

Releases without downtime

Keep going