Measure your test suite

Find your slowest pytest tests

List the slowest pytest tests with --durations and --durations-min, read setup vs call vs teardown times, then fix, split or mark slow tests so they stop holding up the suite.

In most slow suites the time isn't spread evenly. A handful of tests take seconds each while thousands take milliseconds, and those few decide how long you wait, especially under pytest-xdist, where one 40-second test sets the minimum for the whole run. Finding them takes one flag.

List the slowest tests with --durations

$ pytest --durations=20 --durations-min=1.0
============================= slowest 20 durations =============================
8.31s call     tests/test_import.py::test_import_full_catalog
3.02s setup    tests/test_api.py::TestOrders::test_list
2.11s call     tests/test_checkout.py::test_checkout_with_coupon
1.40s teardown tests/test_browser.py::test_login_flow
  • --durations=N shows the N slowest entries; --durations=0 shows all of them.
  • --durations-min=S hides anything shorter than S seconds. The default is 0.005s, so very short phases are left out unless you pass -vv, which sets the minimum to 0.
  • Each line is one phase of one test: setup (fixtures, setUp, setUpClass for the first test of the class), call (the test body) and teardown.

To see them on every run, put the flags in addopts:

# pyproject.toml
[tool.pytest.ini_options]
addopts = ["--durations=10", "--durations-min=1.0"]

Read the output

The phase tells you where to look.

  • Slow call: the test body itself. Usually the code under test, a time.sleep, real network calls, or a test that does the work of ten tests.
  • Slow setup: fixtures and setUp. If it's the first test of a module or class that's slow and the rest are fast, a module, class or session scoped fixture is being built there, and the time is shared by everything after it. If every test in a class has a slow setup, the work is per test and moving it to setUpTestData or a wider fixture scope helps (see function-scoped fixtures and setUpTestData).
  • Slow teardown: cleanup. Flushing tables after a TransactionTestCase, closing browsers, stopping threads that don't stop on their own, removing large temporary directories.

Two things make durations misleading:

  • The first test pays for warm-up. The first test that imports a heavy module, opens the first database connection or fills a cache is slower than the same test run later. Run it alone to see its real cost: pytest "tests/test_api.py::TestOrders::test_list" --durations=0.
  • Durations vary. A test that took 3s on a loaded CI machine might take 1s on your laptop. Look at more than one run before concluding anything.

Find out why it's slow

--durations says how long, not why. For one test, profile it alone (see how to profile a pytest suite), or let flakytest break it down:

$ flakytest run -- "tests/test_checkout.py::test_checkout_with_coupon"
$ flakytest report --test "tests/test_checkout.py::test_checkout_with_coupon"

flakytest report --test prints that test's phases, time by category, its biggest pieces of work, the hot lines in your code and its call tree. You can also pass a file or class prefix to look at a group of tests at once. The usual causes each have a guide:

What the time is Guide
password hashing in create_user / client.login fast password hasher
time.sleep, retries with backoff replace time.sleep
real HTTP calls block network calls
many queries, the same query over and over N+1 queries
building test data faster factories
subprocesses, browsers subprocesses, shared browser

What to do with one slow test

Fix the biggest piece first

Most slow tests have one dominant cost. Fix that before anything else: a test that spends 90% of its time hashing passwords or sleeping gets fast without any restructuring.

Break it down

A test that runs for 8 seconds often tests many things in sequence: it imports a full catalog, then checks prices, then stock, then search. Split it so each test does the setup it needs and checks one thing, and share the expensive, read-only part:

import pytest

from myapp.catalog import import_catalog, search


@pytest.fixture(scope="module")
def small_catalog(tmp_path_factory):
    path = tmp_path_factory.mktemp("catalog") / "catalog.csv"
    path.write_text("sku,name,price\nA1,Apple,1.00\nB2,Banana,0.50\n")
    return import_catalog(path)


def test_prices(small_catalog):
    assert small_catalog["A1"].price == 1


def test_search(small_catalog):
    assert [p.sku for p in search(small_catalog, "ban")] == ["B2"]

Two things usually fall out of this: most checks don't need the full data set (three rows test the parsing as well as 50,000 do), and the parts that fail now fail with a name that says what broke. Keep one test that runs the full import if the size itself matters.

Mark it slow and run it separately

Some tests are slow because they have to be: an end-to-end flow through a browser, a migration test, a test against a real external service. Don't let them slow down every run. Mark them and run them in their own step:

# pyproject.toml
[tool.pytest.ini_options]
markers = ["slow: takes seconds; deselect with -m 'not slow'"]
addopts = ["--strict-markers"]
import pytest


@pytest.mark.slow
def test_full_checkout_in_browser(page): ...

Put pytestmark = pytest.mark.slow at the top of a module to mark all its tests. Then:

$ pytest -m "not slow"   # the fast loop, locally and on every push
$ pytest -m slow         # a separate CI job, or before merging

Registering the marker and using --strict-markers makes a typo like @pytest.mark.slwo an error instead of a test that silently runs in the fast set. Make sure the slow job actually runs in CI: a test nobody runs isn't testing anything.

Track it over time

Slow tests come back. A test that took 0.2s last month takes 4s after someone added a fixture or a sleep. With flakytest.dev, every run from CI and every developer machine is collected, and each test's page shows its average, median and max per environment and a duration chart per run. The dashboard lists the slowest tests and why, and the tests that are much slower in CI than locally. See environments.

How flakytest finds it

At the end of a profiled run flakytest prints the slowest tests with the category and the piece of work that explains each one:

Slowest tests:
    3.21s  tests/test_orders.py::TestCheckout::test_checkout  (database 70%, setUp (call) 2.1s, 412 queries)

It also looks at the three longest tests. When one of them takes at least 2 seconds of its own time (not counting class, module or session fixtures it happened to trigger) and at least 10% of the suite's test time, it suggests a fix titled after the test, like test_checkout alone takes 35% of test time, with its main category and biggest piece of work. The suggested fix is to open its breakdown with flakytest report --test '<test id>', fix the biggest piece first, and if it's an integration test that has to be slow, mark it @pytest.mark.slow and run it in a separate CI job.

Further reading

Related guides

  • How to profile a pytest suite: Profile slow pytest tests with cProfile, pytest-profiling, py-spy, pyinstrument, Python 3.15's tachyon or flakytest: the commands, what each one shows, and which to use when.
  • 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.
  • Expensive function-scoped pytest fixtures: A pytest fixture without a scope runs again for every test that uses it. Give expensive fixtures module or session scope, or split them into a shared part and a cheap reset.
  • 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