# How to speed up a slow pytest suite

> A checklist for slow Python test suites, from the fixes that usually save the most time (password hashing, migrations, test data) to parallelism and CI. Each item links to a detailed guide.

Slow test suites are rarely slow everywhere. Most of the time goes to a handful of causes that repeat across hundreds of tests: hashing the same password, building the same rows, running the same migrations, waiting for the same sleep. Fix one of those and every test that shares it gets faster.

This page is a checklist, roughly in the order the fixes pay off in a typical Django or SQLAlchemy project. Each item links to a guide with the reasoning, before/after code and the caveats that keep your tests correct.

## Measure before you change anything

Guessing where test time goes is usually wrong, and a fix you can't measure is a fix you can't defend in code review.

- **Find the slowest tests** with `pytest --durations=20`, then look inside them. See [find your slowest pytest tests](https://flakytest.dev/guides/slowest-tests/).
- **Profile the whole suite**, not one test: the expensive thing is often spread thinly across every test (a `setUp`, an autouse fixture, a login). See [how to profile a pytest suite](https://flakytest.dev/guides/profile-pytest/).
- **Compare before and after.** Run the suite a few times before and after a change; the noise between runs is often bigger than you'd expect. `flakytest compare` shows which tests got faster or slower, and which changed outcome.

## Database setup

The database is where most web application suites spend their time.

- **Use a fast password hasher.** Django's default PBKDF2 hasher is slow by design, and every `create_user()` and `client.login()` pays for it. One setting fixes it: [use a fast password hasher in tests](https://flakytest.dev/guides/fast-password-hasher-in-tests/).
- **Don't migrate on every run.** Keep the test database between local runs with `--reuse-db` (pytest-django) or `--keepdb` (Django): [stop re-running migrations](https://flakytest.dev/guides/reuse-test-database/).
- **Migrate once, not once per worker or runner.** With pytest-xdist or several CI jobs, every worker and runner builds its own test database: [run migrations once](https://flakytest.dev/guides/run-migrations-once/).
- **Roll back instead of truncating.** `TransactionTestCase` empties every table after each test, `TestCase` rolls back one transaction: [use TestCase instead of TransactionTestCase](https://flakytest.dev/guides/testcase-vs-transactiontestcase/). For SQLAlchemy, [create the schema once and roll back per test](https://flakytest.dev/guides/sqlalchemy-rollback-per-test/).
- **Make the test database server skip durability work.** A throwaway PostgreSQL or MySQL server doesn't need to flush every commit to disk: [fsync=off and friends](https://flakytest.dev/guides/fast-test-database-server/).
- **Reuse connections.** Connecting to a database takes milliseconds, which adds up when it happens in every test: [stop opening new database connections](https://flakytest.dev/guides/reuse-database-connections/).

## Test data

- **Create shared data once per class.** Rows that tests only read belong in `setUpTestData`, not `setUp`: [setUpTestData instead of setUp](https://flakytest.dev/guides/setuptestdata-vs-setup/).
- **Load fixture files once.** JSON and YAML fixtures are parsed and saved row by row: [load Django fixtures once per class](https://flakytest.dev/guides/django-fixture-loading/).
- **Keep class-level data small.** Django deep-copies `setUpTestData` attributes for every test that reads them: [setUpTestData deep copies](https://flakytest.dev/guides/setuptestdata-deepcopy/).
- **Make factories cheaper.** Build instead of create, create in bulk, and reuse one Faker instance: [faster factories](https://flakytest.dev/guides/faster-test-factories/).
- **Widen expensive fixture scopes.** A function-scoped fixture that takes 200ms costs 200ms for every test that uses it: [expensive function-scoped fixtures](https://flakytest.dev/guides/pytest-fixture-scope/).
- **Fix N+1 queries.** Tests that run the same query hundreds of times are slow because the code under test is slow, so the fix speeds up production too: [find and fix N+1 queries](https://flakytest.dev/guides/n-plus-one-queries/).

## Waiting and external services

- **Stop sleeping.** A `time.sleep(1)` in a retry loop costs a second in every test that hits it, and fixed sleeps are a classic cause of flakiness: [replace time.sleep](https://flakytest.dev/guides/sleep-in-tests/).
- **Stop waiting on timeouts.** Threads, queues and event loops that wait for a timeout instead of a signal: [tests blocked on threads and queues](https://flakytest.dev/guides/waiting-in-tests/).
- **Mock the network and block it.** Real HTTP calls are slow and a common reason tests pass locally and fail in CI: [mock and block network calls](https://flakytest.dev/guides/block-network-in-tests/).
- **Use an in-memory cache** instead of Redis or Memcached: [in-memory cache in tests](https://flakytest.dev/guides/in-memory-cache-in-tests/).
- **Run Celery tasks eagerly** instead of through a broker: [Celery eager mode](https://flakytest.dev/guides/celery-eager-in-tests/).
- **Call code instead of spawning processes**: [tests that spawn processes](https://flakytest.dev/guides/subprocesses-in-tests/).
- **Share the browser** between Playwright or Selenium tests: [share the browser](https://flakytest.dev/guides/share-browser-between-tests/).

## Python and library overhead

- **Speed up startup and collection.** Heavy imports in `conftest.py` and pytest walking unrelated directories cost time on every run, even when you run a single test: [slow pytest startup](https://flakytest.dev/guides/slow-pytest-startup/).
- **Check your test settings.** The debug toolbar, `DEBUG = True` and manifest static storage all add overhead: [Django test settings for speed](https://flakytest.dev/guides/django-test-settings/).
- **Replace freezegun with time-machine**: [time-machine](https://flakytest.dev/guides/freezegun-to-time-machine/).
- **Patch once, not in every test**, especially with `autospec`: [expensive mocks](https://flakytest.dev/guides/expensive-mocks/).
- **Share one event loop** between async tests: [pytest-asyncio loop scope](https://flakytest.dev/guides/pytest-asyncio-event-loop-scope/).
- **Tune the garbage collector** for suites that allocate a lot: [gc.freeze in tests](https://flakytest.dev/guides/python-gc-in-tests/).
- **Log less**: [logging in tests](https://flakytest.dev/guides/logging-in-tests/).
- **Parse data files once**: [serialization in tests](https://flakytest.dev/guides/serialization-in-tests/).
- **Measure coverage with sys.monitoring** on Python 3.12+: [COVERAGE_CORE=sysmon](https://flakytest.dev/guides/faster-coverage-sysmon/).

## Parallelism and CI

Parallelism multiplies everything above, so it's worth doing after the fixes that remove work rather than before.

- **Use every CPU** with pytest-xdist: [run tests in parallel](https://flakytest.dev/guides/pytest-xdist/).
- **Balance the workers** so the run doesn't wait on the slowest one: [--dist worksteal](https://flakytest.dev/guides/pytest-xdist-worksteal/).
- **Speed up CI itself**: dependency caching, sharding across runners, and why CI is slower than your laptop: [speed up tests in CI](https://flakytest.dev/guides/speed-up-tests-in-ci/).

## Keep it fast

Suites get slow gradually, one reasonable test at a time. Two habits keep them fast: write tests that don't do more work than they need (see [Django testing best practices](https://flakytest.dev/guides/django-testing-best-practices/) and [pytest fixtures best practices](https://flakytest.dev/guides/pytest-fixtures-best-practices/)), and watch the suite's time as a trend, per environment, so a regression shows up the week it lands rather than the year after. flakytest.dev collects every CI and local run for exactly that.
