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.
- Compatibility: can the currently running code work with the new schema during the deploy?
- Locks: does any operation rewrite or exclusively lock a large table? Use
sqlmigrateto see the SQL. - Indexes on large tables use
AddIndexConcurrentlyin a non-atomic migration. - Data migrations are separate, batched, idempotent, and use historical models (
apps.get_model). - Destructive changes (drop column or table) happen in a later release, after code stops using them.
- 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
RenameFieldon 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 00NNreviewed for each migration.- No
ALTER COLUMN TYPEthat rewrites a large table without a plan (new column + backfill instead). NOT NULLadded to existing large columns via aCHECKconstraintNOT VALID, validated separately, where needed.lock_timeoutset for migrations on busy tables so they fail fast instead of queueing traffic behind them.
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,updateby primary key ranges) or as a management command outside the migration. - The operation is idempotent: rerunning it does no harm.
- A
reverse_codeis provided, ormigrations.RunPython.noopwith 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.
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-linteranddjango-pg-zero-downtime-migrationsautomate parts of this review. - Small tables rarely need special treatment; focus effort on tables with many rows or heavy write traffic.
Next steps
- Plan releases with Django Migrations Deployment Strategy.
- Handle big tables with Django Schema Changes on Large Tables.
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.