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

  1. 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.
  2. Long-running queries on the table you're altering (reports, imports).
  3. The migration itself is heavy: adding an index (blocks writes while it builds), changing a column type (rewrites the table), adding a NOT NULL column with a non-constant default, or a RunPython data migration touching millions of rows in one transaction.
  4. 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

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")),
    ]

Making it stick in production

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

Get startedSee pricing

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.