Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

django makemigrations: The Four Commands That Actually Migrate and the Five That Look Right but Aren't

Sean

Platform Writer

Jun 18, 2026
7 min read

python manage.py makemigrations is the command every Django tutorial shows you. It is the command that creates the migration file. It is also the command that does almost nothing on its own. The full migration story is four commands — makemigrations, migrate, showmigrations, sqlmigrate — and the order you run them, the flags you pass, and the failure modes you hit at deploy time. The honest version covers all four plus the five commands that look right but cause weeks of confusion.

django makemigrations: The Four Commands That Actually Migrate and the Five That Look Right but Aren't

Table of contents

The short version for a clean migration cycle: edit your models, run makemigrations, commit the resulting file, run migrate against your target database. That is the happy path. The unhappy paths are: migration files you forgot to commit, migrations that conflict between branches, migrations that take a lock and block production, migrations that fail halfway through, and migrations that succeed but leave the database in the wrong shape. The four commands below handle the happy path and most of the unhappy paths.

django makemigrations: the four commands that actually migrate and the five that look right but aren't

Table of contents

The direct answer

The four commands in order:

# 1. After editing models.py, generate the migration file
python manage.py makemigrations

# 2. Preview the SQL (optional but recommended)
python manage.py sqlmigrate myapp 0001

# 3. Apply the migration to the database
python manage.py migrate

# 4. Verify the state
python manage.py showmigrations

That is the cycle. Edit models, generate, preview, apply, verify. The migration file is committed to source control and applied to every environment (dev, staging, production) by running migrate against the same migration files.

The four commands that actually migrate

Each of the four has a specific role. They are not interchangeable.

makemigrations — read the models, compare to the last migration, generate the SQL operations needed to make the database match. Writes a new file in app/migrations/0001_initial.py (or the next number). Does not touch the database.

migrate — read the migration files, compare to the django_migrations table in the database, apply any unapplied migrations. Touches the database.

showmigrations — list every app’s migrations and show which are applied ([X]) and which are pending ([ ]). Diagnostic only.

sqlmigrate — print the SQL a specific migration would run, without applying it. Use this to preview a migration before running it, especially in production.

These four together cover 95% of Django migration work. The other 5% is makemigrations --empty, migrate --fake, migrate --plan, and other special-case flags that are useful but rare.

makemigrations: what it does and what it does not

makemigrations does three things:

  1. Reads models.py for every app in INSTALLED_APPS.
  2. Reads the latest migration file in each app’s migrations/ directory.
  3. Computes the difference and writes a new migration file describing the operations.

It does not:

  • Touch the database.
  • Run the migration.
  • Apply the changes.
  • Check whether the migration is safe (it can lock tables, lose data, etc.).

A common confusion: makemigrations “does nothing” if there are no model changes. That is correct. The output is No changes detected. and no file is written. Run it anyway — it costs nothing, and confirms that what you expected is what Django sees.

A second confusion: makemigrations requires the app to be in INSTALLED_APPS and the app’s migrations/ directory to exist with an __init__.py. If you forgot the migrations directory, Django says No migrations directory found and exits.

A third confusion: makemigrations app_name only generates migrations for one app. Without the app name, it generates migrations for every app with pending changes. For a single-app project the difference is invisible; for a multi-app project, scope it.

migrate: the deploy command

migrate is the only command in the four that touches the database. It applies any unapplied migrations in order. In production, this is the command you run as part of your deploy script:

python manage.py migrate --noinput

The --noinput flag prevents Django from prompting for a yes on destructive operations. Always pass it in production.

The flag combination that catches the most deploy issues:

python manage.py migrate --noinput --plan

--plan prints the migrations that would be applied without actually applying them. Use this in a dry-run step before the real migrate to catch surprises.

For multi-database Django (a setup with DATABASES pointing at more than one backend), pass --database=:

python manage.py migrate --database=analytics --noinput

The default is default, which is the one named in DATABASES['default'].

showmigrations: the diagnostic command

showmigrations lists every migration in every app, with an [X] for applied and [ ] for pending:

myapp
 [X] 0001_initial
 [X] 0002_add_user_email
 [ ] 0003_add_user_avatar
auth
 [X] 0001_initial
 [X] 0002_alter_permission_name_max_length
...

Use this when:

  • A migration is missing. The list shows [ ] instead of [X].
  • Two branches diverged. Each branch shows [X] for its own migrations and [ ] for the other branch’s.
  • You are about to deploy and want to confirm what will run.
  • You are debugging a “relation does not exist” error after a partial deploy.

The output is also useful in scripts — manage.py showmigrations | grep "\[ \]" returns a count of pending migrations, which can be a pre-deploy assertion in CI.

sqlmigrate: the preview command

sqlmigrate prints the SQL a migration would run, without applying it:

python manage.py sqlmigrate myapp 0003

Output:

BEGIN;
CREATE TABLE "myapp_userprofile" (
    "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    "user_id" integer NOT NULL UNIQUE REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED
);
CREATE INDEX "myapp_userprofile_user_id_idx" ON "myapp_userprofile" ("user_id");
COMMIT;

Use this before running a migration in production, especially for operations that lock tables (ALTER TABLE ADD COLUMN with a default, large data backfills, etc.). The output is the actual SQL Django will execute, including the wrapping transaction.

For PostgreSQL specifically, the SQL includes BEGIN; and COMMIT; around the operations. Django wraps every migration in a transaction by default. If you have a migration that cannot run in a transaction (e.g. CREATE INDEX CONCURRENTLY), set atomic = False in the migration class.

The five commands that look right but cause pain

makemigrations --merge — looks like a fix for migration conflicts. Sometimes works, often produces a migration that does not reflect the actual schema change. Use it as a last resort, not a first move.

migrate --fake — marks a migration as applied without running it. Use it when you have manually applied a migration (e.g. via psql) and need to tell Django to skip it. Misuse: faking a migration that was never actually run leads to a database that does not match Django’s expectations.

migrate myapp zero — migrates an app backwards to no migrations. This is destructive — it does not undo the SQL, it just marks the migrations as unapplied. Use migrate myapp <previous_migration_name> to actually reverse.

makemigrations --empty — creates an empty migration file. Use it for data migrations that do not change schema (e.g. backfilling a new column). Easy to forget the --empty and end up with a “no changes detected” error.

migrate --run-syncdb — creates tables for apps that are not under migration control. Use only when you have legacy apps that predate Django’s migration system. The default behavior is to skip unmanaged apps.

The migration conflict you hit the first time you work on a team

Two developers edit models.py on separate branches. Both run makemigrations. Both produce 0003_some_change.py. Both commit. Both merge to main. Now the project has two 0003_* files. migrate fails with “Conflicting migrations detected.”

The fix has three steps:

  1. Delete one of the conflicting migrations. Pick the one that does not represent a schema change you want (usually the older one).
  2. Run makemigrations again to regenerate it under a new number (e.g. 0004).
  3. Run migrate to verify the state.

The alternative is --merge, which produces a “merge migration” that depends on both branches. This works for trivial cases but obscures the schema change history.

The real fix is team discipline: only one developer runs makemigrations per logical change. The migration file is committed with the model change. The other developer pulls and does not need to generate a new one.

The deploy checklist for migrations

The six steps to deploy a Django migration without breaking production:

  1. Run makemigrations locally. Commit the file. PR review should include the migration file diff.
  2. Run makemigrations --check --dry-run in CI. Fails the build if there are uncommitted model changes.
  3. Run sqlmigrate locally to preview the SQL. Look for ALTER TABLE on large tables — these can lock.
  4. Take a database backup before deploying. If the migration fails halfway, restore.
  5. Run migrate --noinput --plan as the first step of the deploy script. Log the planned migrations.
  6. Run migrate --noinput as the last step. Run it after the new code is deployed but before the new code is serving traffic.

For multi-instance deploys (blue/green, canary, rolling), the migration must complete before the new code serves traffic. The new code might reference new columns that do not exist yet. The pattern: deploy a backward-compatible schema change, then deploy code that uses it, then deploy a cleanup migration.

For Django apps running on a managed platform with auto-deploys, the migration command is usually a build-step or a release-step command. Most platforms let you specify python manage.py migrate --noinput as the release command. The platform runs it before the new code starts serving.

If you are deploying to RunxBuild’s backend services, the release command is one setting in the dashboard; the build log shows migrate output and any failures stop the deploy. For the cost of running the database, the API, and the worker queue on one platform, the RunxBuild hosting calculator gives you the per-month number against your current setup.

FAQ

What is the difference between makemigrations and migrate?

makemigrations reads models and writes migration files. migrate reads migration files and applies them to the database. The first is local; the second touches the database.

Why does makemigrations say “No changes detected”?

Either you did not edit models.py, or your edits do not affect the schema (renaming a method, adding a docstring, etc.). makemigrations only generates migrations for model changes that affect the database schema.

How do I undo a migration?

python manage.py migrate myapp 0002 reverses all migrations after 0002 for myapp. The data is preserved unless the migration’s reverse operation explicitly removes it.

How do I create a data-only migration?

python manage.py makemigrations --empty myapp. Then edit the generated file to add RunPython operations that backfill data.

How do I see what migrations are pending?

python manage.py showmigrations | grep "\[ \]". Lines starting with [ ] are pending.

How do I handle a migration that takes a long time on a large table?

For PostgreSQL, set atomic = False in the migration class and use RunSQL with CREATE INDEX CONCURRENTLY instead of the default ALTER TABLE ADD INDEX. For data backfills, run them in batches in a separate worker process.

Can I run migrations without downtime?

Yes, with the expand-and-contract pattern: deploy a backward-compatible schema change first, then deploy code that uses the new schema, then deploy a cleanup migration. Django migrations support this natively — most operations are reversible.

What happens if I have a migration conflict in a team?

Delete one of the conflicting 0003_* files, run makemigrations again to regenerate, run migrate. Or use makemigrations --merge for trivial cases. The real fix is team discipline: only one developer runs makemigrations per logical change.

FAQ

What is the difference between makemigrations and migrate?

makemigrations reads models and writes migration files. migrate reads migration files and applies them to the database.

Why does makemigrations say “No changes detected”?

Either you did not edit models.py, or your edits do not affect the schema (renaming a method, adding a docstring, etc.).

How do I undo a migration?

python manage.py migrate myapp 0002 reverses all migrations after 0002 for myapp. The data is preserved unless the reverse operation removes it.

How do I create a data-only migration?

python manage.py makemigrations --empty myapp. Edit the generated file to add RunPython operations.

How do I see what migrations are pending?

python manage.py showmigrations | grep "\[ \]". Lines starting with [ ] are pending.

How do I handle a migration that takes a long time on a large table?

Set atomic = False in the migration class and use CREATE INDEX CONCURRENTLY. For data backfills, run them in batches in a worker.

Can I run migrations without downtime?

Yes, with the expand-and-contract pattern: deploy a backward-compatible schema change first, then code that uses it, then cleanup.

What happens if I have a migration conflict in a team?

Delete one of the conflicting files, run makemigrations to regenerate. Or use --merge for trivial cases. The real fix is team discipline.

#Django#Migrations#Python#ORM#Database