Guides / python manage.py migrate hangs or locks the table
python manage.py migrate hangs or locks the table
You run python manage.py migrate in a deploy and it sits there for minutes, never finishes, or times out your pipeline. On Postgres, a migration that waits is almost always waiting for a lock held by something else. Find the blocker, then change how you write the migration.
What it means
Many schema changes need an exclusive lock on the table. If another transaction is using the table (even one that's just sitting idle inside an open transaction), the migration queues behind it. Worse, while the migration queues, every new query on that table queues behind it, so one stuck migration can freeze your whole site.
Why it happens
- An idle-in-transaction connection holds a lock. A shell session, a script, or an old app instance that opened a transaction and never finished it.
- Long-running queries on the table you're altering (reports, imports).
- The migration itself is heavy: adding an index (blocks writes while it builds), changing a column type (rewrites the table), adding a
NOT NULLcolumn with a non-constant default, or aRunPythondata migration touching millions of rows in one transaction. - The old version is still serving and holding connections while the new one migrates.
The fix
1. See who is blocking
SELECT pid, state, wait_event_type, now() - xact_start AS xact_age, left(query, 80) AS query
FROM pg_stat_activity
WHERE datname = current_database() AND state <> 'idle'
ORDER BY xact_start;
Look for a row in state idle in transaction that is old. If it is safe to remove, end it with SELECT pg_terminate_backend(<pid>);. Be careful with anything you don't recognize.
2. Fail fast instead of hanging
Set a lock timeout for the migration run, so a blocked migration errors out in seconds and you can retry, instead of freezing the site:
# settings.py (or a dedicated migration settings module)
DATABASES["default"]["OPTIONS"] = {"options": "-c lock_timeout=5000"} # 5 seconds, in ms
3. Write migrations that don't lock for long
- Add indexes concurrently on busy tables. It needs a non-atomic migration:
from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False # required for CONCURRENTLY
dependencies = [("shop", "0012_previous")]
operations = [
AddIndexConcurrently("order", models.Index(fields=["created"], name="order_created_idx")),
]
- Add columns as nullable first, backfill in batches in a separate step, and add
NOT NULLlater. - Batch data migrations (update thousands of rows at a time) and set
atomic = Falseon those migrations. - Avoid column type changes on big tables in a single step; add a new column, copy, switch, and drop the old one in a later release.
Making it stick in production
- Run migrations as a single release step before the new code starts serving (unapplied migrations), with a timeout on the step, so a stuck migration stops the release and doesn't wait forever.
- Inspect the SQL before it runs:
python manage.py sqlmigrate shop 0013. - Rehearse big migrations on a copy of the production data, and time them.
- Take a backup first. If something goes wrong halfway, see how to roll back a Django deploy.
Or skip it
Django Cloud runs migrate as the release command before the new version starts and shows its output in the release logs. If the release fails, the previous one keeps serving. Writing lock-friendly migrations is still up to you.
pip install djangocloud-cli
djangocloud deploy
Free plans are available for students, AWS Activate startups, open-source, non-profit and research projects. See how Django Cloud compares with other ways to host Django.
Django is a registered trademark of the Django Software Foundation. Django Cloud is not affiliated with or endorsed by it. Found a mistake in this guide? Tell us and we will fix it.