# 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

```console
$ 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`:

```toml
# 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](https://flakytest.dev/guides/pytest-fixture-scope/) and [setUpTestData](https://flakytest.dev/guides/setuptestdata-vs-setup/)).
- **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](https://flakytest.dev/guides/profile-pytest/)), or let flakytest break it down:

```console
$ 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](https://flakytest.dev/guides/fast-password-hasher-in-tests/) |
| `time.sleep`, retries with backoff | [replace time.sleep](https://flakytest.dev/guides/sleep-in-tests/) |
| real HTTP calls | [block network calls](https://flakytest.dev/guides/block-network-in-tests/) |
| many queries, the same query over and over | [N+1 queries](https://flakytest.dev/guides/n-plus-one-queries/) |
| building test data | [faster factories](https://flakytest.dev/guides/faster-test-factories/) |
| subprocesses, browsers | [subprocesses](https://flakytest.dev/guides/subprocesses-in-tests/), [shared browser](https://flakytest.dev/guides/share-browser-between-tests/) |

## 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:

```python
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:

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

```python
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:

```console
$ 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](https://flakytest.dev/docs/#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:

```console
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

- [pytest: profiling test execution duration](https://docs.pytest.org/en/stable/how-to/usage.html#profiling-test-execution-duration)
- [pytest: `--durations` and `--durations-min`](https://docs.pytest.org/en/stable/reference/reference.html#command-line-flags)
- [pytest: working with custom markers](https://docs.pytest.org/en/stable/example/markers.html)
- [pytest: registering markers](https://docs.pytest.org/en/stable/how-to/mark.html#registering-marks)
