What are your go-to tricks for speeding up a Django test suite?

Hey everyone,

As our codebase has grown, our Django test suite execution time has slowed down significantly, and it’s starting to bottleneck our local feedback loops and CI/CD pipelines.

We are already using two major optimization strategies:

  1. Running our test cases in 8 parallel processes (--parallel=8).

  2. Using FactoryBoy for handling ORM setup more efficiently than standard Django fixtures.

Even with these, the suite is sluggish. We want to implement deeper, structural optimizations. What are your go-to advanced strategies for squeezing extra speed out of a growing Django test suite?

My default would be to recommend Adam Johnson’s book: Speed Up Your Django Tests

Two things from top of my head:

  • Skip migrations during test setup (aka don’t let Django execute all migrations one by one, but rather reconstruct final db directly)
  • Don’t sleep on durability settings for your database, e.g. for Postgres you can make like 30 % speedup just from good db settings: Less durability = faster tests - by Petr Stribny

Hi how’re you feeling chief?
Great question here are the structural optimizations that usually give the biggest wins on a large Django suite, roughly in order of impact:

  1. keepdb (huge, often overlooked)
    Recreating the test database and re-running all migrations on every run is expensive. Reuse it:

python3 manage.py test --parallel=8 --keepdb

Combine it with disabling migrations entirely for tests build the schema straight from the models instead of replaying migration history:

settings/test.py
class DisableMigrations:
def contains(self, item): return True
def getitem(self, item): return None

MIGRATION_MODULES = DisableMigrations()

  1. Use a fast password hasher
    Argon2/bcrypt are intentionally slow. Any test that creates a user pays that cost:

PASSWORD_HASHERS = [“django.contrib.auth.hashers.MD5PasswordHasher”]

This alone can shave a surprising amount off auth-heavy suites.

  1. setUpTestData instead of setUp
    setUp runs before every test. setUpTestData runs once per class and is wrapped in a transaction rolled back between tests so shared read-only fixtures are created a single time:

class OrderTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.customer = CustomerFactory()

  1. Prefer TestCase over TransactionTestCase
    TestCase wraps each test in a transaction and rolls back far faster than truncating tables. Only use TransactionTestCase when you genuinely need to test transaction behavior.

  2. Tighten FactoryBoy usage

  • Use .build() (no DB hit) instead of .create() whenever you don’t need persistence.
  • Watch out for SubFactory chains silently creating deep object graphs — pass existing instances instead.
  • Use django_get_or_create to avoid redundant inserts.
  1. Profile before optimizing further
    Find the actual offenders rather than guessing:

with pytest-django
pytest --durations=20

or django-slowtests for the vanilla runner. Often 10% of the tests eat 80% of the time.

  1. Mock external I/O
    Any real network/API/filesystem call in a test is a hidden slowdown (and a flakiness source). Mock them.

  2. Split the suite
    Fast unit tests for the local loop, slower integration tests gated in CI. Devs get feedback in seconds; the full run happens off the critical path.

One caveat on parallelism: --parallel=8 only helps up to your available cores — going past physical CPUs can slow things down from context-switching, so benchmark it.