Remove a Django Field Without Downtime
Drop a Django model field and its database column safely during rolling deploys: stop using the field in code first, remove it from Django’s state with SeparateDatabaseAndState, drop the column in a later release, and handle NOT NULL columns and dependent indexes.
Problem statement
You delete a field from a model, run makemigrations, and deploy. The migration drops the column while old application instances are still running. Django selects every model column by name, so old instances fail on every query of that model with column "legacy_code" does not exist, not just where the field was used. Removing a field safely means removing it from code before removing it from the database.
- Release 1: stop reading and writing the field anywhere (views, forms, serializers, admin, raw SQL).
- Release 2: remove the field from the model, but keep the column with
SeparateDatabaseAndState; if the column isNOT NULLwithout a default, make it nullable first. - Release 3: drop the column with a migration once no running code knows about it.
- Drop dependent indexes and constraints concurrently if the table is large.
- Check other consumers: reports, other services, ETL jobs, and database views.
Step-by-step solution
1. Stop using the field
Search the codebase for the field name, including templates, serializers, values() calls, raw SQL, and admin list_display. Deploy code that no longer reads or writes it. The column stays in the database and in the model for now, so old and new code both work.
2. Make NOT NULL columns safe for inserts
If the column is NOT NULL without a database default, new code that no longer sets it will fail on insert once the field is gone from the model. Before removing the field, make it nullable (a fast metadata change on PostgreSQL):
legacy_code = models.CharField(max_length=20, null=True)
Or give it a database default with db_default (Django 5.0+).
3. Remove the field from Django's state only
class Migration(migrations.Migration):
dependencies = [("orders", "0050_order_legacy_code_nullable")]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[migrations.RemoveField(model_name="order", name="legacy_code")],
database_operations=[],
),
]
Django stops including the column in queries, but the column still exists. Old instances that still have the field in their model can keep selecting it during the rollout.
4. Drop the column later
After release 2 is deployed everywhere (web, workers, scheduled jobs):
class Migration(migrations.Migration):
dependencies = [("orders", "0051_remove_order_legacy_code_state")]
operations = [
migrations.RunSQL(
"ALTER TABLE orders_order DROP COLUMN IF EXISTS legacy_code;",
reverse_sql=migrations.RunSQL.noop,
),
]
DROP COLUMN on PostgreSQL is fast (it only marks the column dropped) but needs a brief ACCESS EXCLUSIVE lock. Set a lock_timeout so it fails fast instead of queueing behind long transactions and blocking everyone:
migrations.RunSQL("SET lock_timeout = '5s';"),
5. Handle indexes and foreign keys
Indexes on the column are dropped with it. On very large tables, drop them first with RemoveIndexConcurrently to avoid doing it under the exclusive lock. For a ForeignKey, also check constraints and that no other code joins through it.
Explanation
Django's ORM generates explicit column lists for SELECT statements based on the model definition each process loaded at startup. During a rolling deploy, processes with the old model definition still list the dropped column and fail. Removing the field from the model state first (while keeping the column) makes all processes stop referencing it; only then is the physical drop harmless. This is the contract half of the expand-and-contract approach.
Edge cases / notes
- Rollbacks. Once the column is dropped, rolling back to code that uses it is impossible without restoring data; drop only after you are sure.
- Deferred fields. Code using
.only()or.defer()still breaks if the field name is referenced. - MySQL.
DROP COLUMNrebuilds the table on older MySQL versions; use online DDL or tools like gh-ost on large tables.
Next steps
- Rename instead of remove with Rename a Django Model Field Without Downtime.
- Plan releases with Zero-Downtime Django Deployment.
- Handle locks on big tables with Run Django Schema Changes on Large Tables.
FAQ
Why does removing an unused field break the site during deploy?
Old processes still select the column on every query of that model. Remove the field from code first, drop the column later.
What does SeparateDatabaseAndState do here?
It removes the field from Django's model state without running any SQL, so the column stays until a later migration drops it.
Is DROP COLUMN slow on PostgreSQL?
No, it only marks the column dropped, but it needs a brief exclusive lock; use a lock timeout.
Can I roll back after dropping the column?
Only by restoring the data from a backup. Make sure no release needs it before dropping.