Django migration circular reference: detection and resolution

Unexpected migration failures in Django usually appear when circular foreign‑key definitions create a dependency loop. This leads Django to abort the migration, silently breaking downstream deployment pipelines.

# Example showing the issue
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
import django
django.setup()

# Simulate two apps with circular FK migrations
from django.core.management import call_command

print('Running migrations that contain a circular reference...')
try:
    call_command('migrate')
except Exception as e:
    print('Migration error:', e)
# Output shows: django.db.utils.ProgrammingError: ... circular dependency ...

Two migration files reference each other’s model via a foreign key before either table exists. Django builds a migration graph, detects a cycle, and aborts to avoid creating invalid constraints. This mirrors SQL’s requirement that referenced tables be created first. Related factors:

  • ForeignKey defined in both apps pointing to the other
  • No explicit RunPython to create constraints later
  • Auto‑generated migrations that assume linear order

Django will detect the cycle automatically when you run migrate and show an error like:

django.db.migrations.exceptions.CircularDependencyError: 
  app_a.0002_auto -> app_b.0001_initial
  app_b.0002_auto -> app_a.0001_initial

You can also check manually by examining your migration files for mutual dependencies between apps.

Fixing the Issue

Temporary Workaround (Development Only)

# Make the FK nullable to break the cycle
class Migration(migrations.Migration):
    operations = [
        migrations.AlterField(
            model_name='modela',
            name='b',
            field=models.ForeignKey('appb.ModelB', null=True, on_delete=models.CASCADE)
        ),
    ]

Do NOT leave this in production. This temporarily breaks the cycle but leaves your database with nullable foreign keys.

Production Solution: Split Into Separate Migrations

The correct approach is to create tables first, then add foreign keys in a later migration:

Migration 1 (app_a/migrations/0001_initial.py):

class Migration(migrations.Migration):
    operations = [
        migrations.CreateModel(
            name='ModelA',
            fields=[
                ('id', models.AutoField(primary_key=True)),
                ('name', models.CharField(max_length=100)),
            ],
        ),
    ]

Migration 2 (app_b/migrations/0001_initial.py):

class Migration(migrations.Migration):
    operations = [
        migrations.CreateModel(
            name='ModelB',
            fields=[
                ('id', models.AutoField(primary_key=True)),
                ('title', models.CharField(max_length=100)),
            ],
        ),
    ]

Migration 3 (app_a/migrations/0002_add_fk.py):

class Migration(migrations.Migration):
    dependencies = [
        ('app_a', '0001_initial'),
        ('app_b', '0001_initial'),
    ]
    operations = [
        migrations.AddField(
            model_name='modela',
            name='b',
            field=models.ForeignKey('appb.ModelB', on_delete=models.CASCADE),
        ),
    ]

This matches Django’s real migration workflow: create tables in separate apps, then add cross-references.

What Doesn’t Work

❌ Setting db_constraint=False on the FK: This disables the constraint entirely, risking data integrity

❌ Running migrate --fake-initial: Masks the circular dependency without fixing the schema

❌ Switching to manage.py migrate --run-syncdb: Creates tables without respecting migration order, leading to missing constraints

  • Adding foreign keys in the same migration that creates both tables
  • Relying on auto‑generated migrations without reviewing dependencies
  • Using --fake to bypass the error, which hides the real problem

When NOT to optimize

  • Prototype projects: Early‑stage code where schema changes are frequent and the migration graph is short.
  • One‑off data imports: Scripts that run once and are discarded after data load.
  • Read‑only replicas: Environments that never apply migrations, only serve data.
  • Legacy monoliths with fixed schema: When the circular reference is intentional and documented.

Frequently Asked Questions

Q: Why do Django migrations abort with a circular reference?

Migration graph contains a cycle. Django can’t create FK constraints before both tables exist.

Q: Is this a Django bug?

No. Relational databases require linear creation order for constraints.

Q: How can I prevent circular dependencies in future migrations?

Split FK creation into a separate migration after both tables exist.

Q: Can I use --skip-checks to ignore the error?

No. This creates inconsistent database state.


Circular migration dependencies break deployments when Django can’t determine a valid creation order. The solution is always the same: separate table creation from foreign-key addition across multiple migrations. Most Django projects hit this once during initial development, fix it properly, and never see it again.

Why Django middleware order changes request handling