Speed up your test suite

Stop re-running migrations on every test run

Django runs every migration before the first test, on every run. Keep the test database with --reuse-db or --keepdb, or skip migrations with --nomigrations.

Every time you run Django tests, the test runner creates a fresh database (test_<name>), runs every migration in the project against it, runs the tests and then drops the database again. A project with a few hundred migrations can spend tens of seconds on this before the first test starts, and you pay it in full even for pytest tests/test_orders.py::test_one.

The schema almost never changes between two runs on your machine, so most of that work is thrown away and redone.

Why migrations take so long

Running a migration is more than executing its SQL. For each one Django:

  • loads the migration graph and works out the plan
  • replays the operations into an in-memory project state and renders model classes from it, which gets slower as the number of models grows
  • executes the DDL, one statement at a time
  • runs any RunPython data migrations

After the last migration, the post_migrate signal creates content types and permissions for every model. None of this depends on how many tests you run, so it dominates short runs.

pytest-django: --reuse-db

With --reuse-db, pytest-django doesn't drop the test database at the end of the run, and the next run uses the existing one. Put it in your default options:

# pyproject.toml
[tool.pytest.ini_options]
addopts = "--reuse-db"

The first run creates and migrates the database as before. Later runs connect to it and start almost immediately. Django still runs migrate on the kept database, so new migrations you add are applied, and when nothing is new that step is cheap.

What it doesn't notice: a migration you edited after it was applied, migrations applied on another branch, and schema changes made without a migration. In those cases rebuild it once:

$ pytest --create-db

With pytest-xdist each worker keeps its own database (test_<name>_gw0, test_<name>_gw1, ...), so the first run with a new worker count builds the new ones.

Django's test runner: --keepdb

manage.py test has the same option:

$ python manage.py test --keepdb
$ python manage.py test --keepdb --timing   # prints how long database setup took

The behaviour matches --reuse-db: the database survives the run and unapplied migrations are applied at the start of the next one. Drop the flag once to rebuild from scratch.

Skip migrations: --nomigrations

--reuse-db makes repeated runs fast but the first build still runs every migration. If no test depends on what migrations do beyond creating tables, pytest-django can build the schema straight from your models:

$ pytest --nomigrations   # alias of --no-migrations

The Django test runner equivalent is the MIGRATE test setting:

# test settings
DATABASES["default"].setdefault("TEST", {})["MIGRATE"] = False

This is faster, but the database is no longer built the way production's is:

  • rows created by data migrations don't exist (default groups, sites, lookup tables)
  • anything only a migration creates is missing: RunSQL triggers, functions, extensions, hand-written indexes
  • nothing checks that your migrations still produce the schema your models describe

Use it locally if it fits, and keep CI on migrations.

Make the first build cheaper

If fresh builds are slow because an app has hundreds of migrations, squashmigrations collapses a range of them into one, which removes much of the per-migration overhead:

$ python manage.py squashmigrations orders 0001 0140

Read the squashing docs first: squashed migrations need care while old ones are still deployed somewhere.

Outside Django: SQLAlchemy and Alembic

The same idea applies: build the schema once per session, not per test, and keep the database between runs. Alembic's upgrade head only applies revisions the database doesn't have yet, so against a persistent test database it's nearly free after the first run:

# conftest.py
import pytest
from alembic import command
from alembic.config import Config
from sqlalchemy import create_engine

TEST_DATABASE_URL = "postgresql+psycopg://postgres:postgres@localhost/myapp_test"


@pytest.fixture(scope="session")
def engine():
    config = Config("alembic.ini")
    config.set_main_option("sqlalchemy.url", TEST_DATABASE_URL)
    command.upgrade(config, "head")
    engine = create_engine(TEST_DATABASE_URL)
    yield engine
    engine.dispose()

This assumes your env.py reads sqlalchemy.url from the config, as Alembic's default template does. Without Alembic, metadata.create_all(engine) in the same session fixture works too. Then roll back each test instead of recreating tables: see create the SQLAlchemy schema once, roll back per test.

Keep the tests correct

  • CI should still build the database from scratch, so a broken migration fails the build. A fresh CI database service already does that; on runners that keep state between jobs, pass --create-db (or drop --keepdb).
  • After switching branches or editing a migration, run once with --create-db.
  • If you use --nomigrations locally, keep migrations on in CI, and add python manage.py makemigrations --check --dry-run to catch model changes without a migration.
  • With several pytest-xdist workers or CI shards, each one still migrates its own database on a fresh run. Run migrations once covers that.

How flakytest finds it

flakytest puts time spent in Django migrations and Alembic in a migrations category. When it's at least a second and 1% of test time, it suggests Stop re-running migrations on every test run, with the time per run (under pytest-xdist, the total across workers and the time each). The fix it shows depends on your setup: --reuse-db and --nomigrations with pytest-django, --keepdb with the Django runner, a session-scoped schema otherwise.

Database setup is a session fixture, so the report charges it to the first test that needed the database and marks it as shared. If one test looks slow only when it runs first, this is often why. Measure with flakytest run before and after adding --reuse-db, then flakytest compare.

Further reading

Related guides

  • Run migrations once with pytest-xdist and CI shards: With pytest-xdist every worker builds its own Django test database, and every CI shard does too. Migrate once and copy it with PostgreSQL template databases or pg_dump.
  • Fix slow pytest startup and heavy imports in tests: pytest takes seconds before the first test runs? Find slow imports with python -X importtime, set testpaths, trim conftest.py imports and disable plugins you don't use.
  • Create the SQLAlchemy schema once, roll back per test: Running metadata.create_all and drop_all for every test is slow. Create the SQLAlchemy schema once per session and roll back each test with join_transaction_mode="create_savepoint".
  • Speed up pytest in CI: Speed up slow CI test jobs: cache dependencies with uv or pip, use pytest-xdist on every core, shard with pytest-split, migrate once, and see why CI is slower than local.

Updated October 18, 2026 · Suggest a change