Skip to content
compiler.dev

Speed Up Django in GitHub Actions

Last updated

To speed up Django in GitHub Actions, cache your Python dependencies, run tests in parallel with pytest-xdist, and stop rebuilding the test database more often than you need. The default python manage.py test runs one process and applies every migration to a new database, which is slow once you have many migrations.

This guide covers install, database setup and test execution, with a workflow you can copy. For the general picture, read our guide to speeding up GitHub Actions.

Where the time goes

Check the step timings on a recent run. In a Django project the pieces are usually: install dependencies, create the test database (applying all migrations), and run the tests. If you have a frontend build or collectstatic, add that too. Test database setup is the item people forget; with hundreds of migrations it can take longer than the tests.

Cache dependencies

With pip, actions/setup-python has a cache input. Its README lists pip, pipenv and poetry, and the key comes from requirements.txt or pyproject.toml by default. Use cache-dependency-path if your files live elsewhere.

- uses: actions/setup-python@v7
  with:
    python-version: "3.13"
    cache: pip
    cache-dependency-path: requirements/*.txt
- run: pip install -r requirements/test.txt

If you use uv, astral-sh/setup-uv has an enable-cache input and a cache-dependency-glob input whose defaults include uv.lock and pyproject.toml. Pin the action as its README shows, then run uv sync --locked. uv installs much faster than pip, which helps most on a cold cache.

If a package compiles from source in CI, switch to its binary build (for example psycopg[binary]) or pin a version that ships wheels.

Run tests in parallel with pytest-xdist

If you use pytest with pytest-django, add pytest-xdist and run with -n auto, which starts one worker per CPU core. pytest-django creates a separate test database per worker (test_db_gw0, test_db_gw1, and so on), so workers do not collide.

# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = config.settings.test
addopts = -n auto --reuse-db --dist loadfile
  • --reuse-db keeps the test database between runs. In CI the database is fresh each run, so it matters little there, but it makes local runs much faster. After model changes, run once with --create-db.
  • --dist loadfile keeps tests from one file on one worker, which is safer when tests share module-level fixtures. The default load mode balances better but can expose order dependence.
  • With Django's own runner, python manage.py test --parallel does the same job with its own database cloning.

Our pytest slow tests guide covers finding slow tests with --durations=20 and fixture scope.

Postgres as a service

Do not test against SQLite if you deploy on Postgres; it hides bugs. Use a service container with a health check:

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: app
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 5s --health-timeout 5s --health-retries 10
    env:
      DATABASE_URL: postgres://postgres:postgres@localhost:5432/app
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: "3.13"
          cache: pip
      - run: pip install -r requirements.txt
      - run: pytest

A throwaway test database does not need durability. If you run Postgres yourself in CI you can start it with -c fsync=off -c synchronous_commit=off -c full_page_writes=off. Never use those settings for real data.

Speed up migrations in tests

Two approaches help with long migration histories:

  • Squash old migrations with squashmigrations when you no longer need the intermediate states.
  • Skip migrations in tests with a settings class that makes MIGRATION_MODULES return None for each app, so tables are built straight from models. This is fast, but you stop testing migrations. Keep one CI job that runs python manage.py migrate on an empty database.

Also mock slow external calls such as email and HTTP.

Split across jobs

When one machine is not enough, shard files across a matrix. pytest-split and pytest-xdist combine well: each matrix job runs a slice chosen by recorded durations and uses -n auto inside. See test sharding in GitHub Actions and the test shard calculator.

What more CPU cores buy you

Django tests use the cores that -n auto finds. On a default hosted runner with 2 vCPU that means two workers. On 8 vCPU it means eight, as long as your tests are not waiting on the database. Postgres in a service container shares the same machine, so very high worker counts can make the database the limit. Watch for that rather than guessing.

We have no Django row in our benchmarks. The closest data is a SvelteKit + FastAPI + Postgres + Redis stack that went from 3m46s to 2m18s (1.6x, full run, 8 October 2026). It is a different framework, so read it as a hint that Python-heavy pipelines gain less than compiled builds.

To try your own suite, change one line to runs-on: [self-hosted, Linux, compiler-ci]. See pricing and compare. Related: the Django template, the Python template and GitHub Actions caching.

FAQ

Does pytest-xdist work with Django?

Yes, through pytest-django, which creates one test database per xdist worker. Use -n auto and make sure tests do not share files or external state.

What does --reuse-db do?

It keeps the test database instead of recreating it. It saves time locally. On a fresh CI machine the database does not exist yet, so it helps little there.

Should I use pip or uv in CI?

uv installs faster and has a built-in cache option in its setup action. pip with actions/setup-python caching is fine for small dependency sets.

How many xdist workers should I use?

-n auto matches CPU cores. If the database is the bottleneck, try a lower number such as half the cores.

Run this pytest suite on compiler.dev 200 free minutes

Start free