Squash Django Migrations Safely in Production
Squash long Django migration histories without breaking deployed environments: run squashmigrations, keep replaced migrations until every database has applied them, handle RunPython and elidable operations, update dependencies, and remove old files in a later release.
Problem statement
An app has 300 migrations. Test databases take minutes to build, makemigrations slows down, and the history is full of fields added and removed years ago. Django's squashmigrations combines a range into one migration, but done carelessly it breaks environments: deleting the old files too early causes NodeNotFoundError, and squashing data migrations can silently drop logic that some database still needs.
- Make sure every environment (production, staging, CI, developer databases) has applied all migrations in the range you will squash.
- Run
python manage.py squashmigrations orders 0250(optionally with a start migration). - Review the generated file: optimize or remove
RunPythonoperations markedelidable, and check dependencies. - Deploy with both the squash and the original files present; Django uses the squash for new databases and the originals elsewhere.
- In a later release, delete the replaced files, remove the
replacesattribute, and update any dependencies that pointed at them.
Step-by-step solution
1. Check that all databases are up to date
python manage.py showmigrations orders | grep "\[ \]"
Run this against every long-lived database. A database that has applied only part of the range can still use the original migrations as long as they exist, but you cannot delete them until it catches up.
2. Generate the squash
python manage.py squashmigrations orders 0250 --squashed-name initial_to_0250
Django creates 0001_squashed_0250_initial_to_0250.py with a replaces list naming every migration it covers, and runs its optimizer to merge operations (for example AddField followed by RemoveField cancels out).
3. Review RunPython and RunSQL
Data migrations are copied into the squash unless marked elidable=True, in which case the optimizer drops them. Old data migrations that only fixed historical data are usually safe to elide, because new databases start empty. Mark them before squashing:
migrations.RunPython(backfill_slugs, migrations.RunPython.noop, elidable=True)
Squashed RunPython operations reference functions in the old migration modules; Django copies the code into the squash only partially, so check that functions still import after you delete the originals. Replace references with inline functions in the squash file if needed.
4. Deploy with both versions present
With the squash and originals in the code:
- New databases (tests, new developers) apply the single squashed migration.
- Existing databases that already applied all replaced migrations get the squash marked as applied automatically.
- Databases partway through continue with the originals.
Run the full test suite and migrate on a copy of production before deploying.
5. Remove the originals later
After every environment has run the release containing the squash:
- Delete the replaced migration files.
- Remove the
replacesattribute from the squash so it becomes a normal migration. - Update other apps' migrations that depended on a replaced migration to depend on the squash.
grep -rn "('orders', '0" */migrations/*.py | grep -v orders/migrations
Explanation
A squashed migration is an optimized replacement for a range of migrations. Its replaces list lets Django's migration loader treat it as equivalent: if all replaced migrations are recorded as applied, the squash is considered applied; if none are, the squash is applied as one step; if only some are, the loader uses the originals. That transitional behavior is why both must coexist until every database has moved past the range. Once they have, the squash can stand alone.
Edge cases / notes
- Circular dependencies. Squashing apps with cross-app dependencies can produce cycles; squash in smaller ranges or start after the cross-app migrations.
- Resetting instead. Deleting all migrations and running
makemigrationsagain is tempting but requires faking initial migrations on every database; squashing is safer. - Third-party apps. Never squash migrations of installed packages; that is their maintainers' job.
Next steps
- If files were removed too early, see Fix Django NodeNotFoundError.
- For mismatched histories, read Fix Django InconsistentMigrationHistory.
- Review migration practices in Django Migrations Deployment Strategy.
FAQ
When can I delete the replaced migration files?
After every database that matters has applied the release containing the squash.
Does squashing change my database schema?
No. It produces the same final schema with fewer steps; existing databases just record the squash as applied.
What happens to data migrations when squashing?
They are kept unless marked elidable=True, in which case the optimizer removes them.
Is it better to reset migrations from scratch?
Usually not on a deployed project. Squashing keeps existing databases consistent without faking migrations.