Speed up your test suite

Tests are sleeping: replace time.sleep

Fixed time.sleep and asyncio.sleep calls make pytest suites slow and flaky. Poll for the condition, patch the sleep in retry and backoff code, and move the clock with time-machine.

A time.sleep(2) in a test does nothing for two seconds. It's there because the test waits for something: a background thread to finish, a cache entry to expire, a retry to happen. The sleep is a guess at how long that takes, and the guess has to be pessimistic, so the test almost always waits longer than it needs to.

Sleeps add up quickly. Ten tests with a one-second sleep cost ten seconds on every run, and sleeps hidden in application code (retry loops, backoff, rate limiters) run in every test that reaches them. They're also a classic source of flaky tests: a sleep long enough on your laptop can be too short on a busy CI runner.

Where the sleeps come from

  • Waiting for a side effect: start_worker(); time.sleep(1); assert job.done.
  • Retry and backoff code under test: an HTTP client that retries three times with 1, 2 and 4 second delays spends 7 seconds in a test that checks the failure path.
  • Waiting for time to pass: a token that expires after 1 second, a cache with a TTL, a "created more than 5 minutes ago" query.
  • asyncio.sleep() in async tests and code, for the same reasons.
  • Fixed waits in browser tests, like Playwright's page.wait_for_timeout().

Each needs a different fix.

Poll for the condition instead

If the test waits for something to happen, check for it in a loop with a short interval and a timeout. The test continues as soon as the condition is true, and the timeout only matters when the test is going to fail anyway:

import time


def wait_until(condition, timeout=5.0, interval=0.01):
    deadline = time.monotonic() + timeout
    while not condition():
        if time.monotonic() >= deadline:
            raise AssertionError(f"condition not met within {timeout}s")
        time.sleep(interval)
# Before: always waits a full second, and fails if the worker is slower
start_worker(queue)
time.sleep(1)
assert queue.empty()

# After: returns as soon as the queue is drained
start_worker(queue)
wait_until(queue.empty)

A generous timeout (5 or 10 seconds) costs nothing when the test passes, and it makes the test more reliable on slow machines than any fixed sleep.

Even better is to not poll at all: have the code signal completion (threading.Event, a future, a callback) and wait on that. See tests blocked on threads, queues and the event loop. In browser tests, use the framework's auto-waiting assertions (Playwright's expect(locator).to_be_visible()) instead of fixed timeouts; see end-to-end testing best practices.

Patch the sleep in retry and backoff code

When the code under test sleeps between retries, the delay is irrelevant to the test. Replace the sleep with a mock and assert how often the code retried:

from unittest import mock

import pytest

from myapp import payments


def test_gives_up_after_three_attempts():
    with (
        mock.patch("myapp.payments.time.sleep") as sleep,
        mock.patch("myapp.payments.send", side_effect=ConnectionError) as send,
    ):
        with pytest.raises(payments.PaymentFailed):
            payments.charge(order_id=1)

    assert send.call_count == 3
    assert sleep.call_args_list == [mock.call(1), mock.call(2)]

mock.patch("myapp.payments.time.sleep") patches the attribute on the time module itself, so every time.sleep call during the with block is mocked. If the module does from time import sleep, patch myapp.payments.sleep instead.

A cleaner long-term fix is to make the delay configurable and set it to zero in test settings (RETRY_BACKOFF = 0), or to pass it in as an argument. With tenacity, override the wait strategy for one call with retry_with:

from tenacity import wait_none

payments.charge.retry_with(wait=wait_none())(order_id=1)

For async code, patch asyncio.sleep where the module uses it, with mock.AsyncMock.

Move the clock instead of waiting for it

If the test waits for time to pass (expiry, TTLs, "older than" logic), move the clock with time-machine:

import datetime as dt

from myapp.tokens import create_token, is_valid


def test_token_expires(time_machine):
    time_machine.move_to(dt.datetime(2026, 1, 1, 12, 0, tzinfo=dt.UTC), tick=False)
    token = create_token(ttl=dt.timedelta(minutes=5))
    assert is_valid(token)

    time_machine.shift(dt.timedelta(minutes=6))
    assert not is_valid(token)

The test covers a six-minute window in microseconds. time-machine mocks wall-clock functions like time.time() and datetime.now(), but not time.monotonic(), so code that measures timeouts with the monotonic clock still needs a configurable timeout or an injected clock. freezegun also moves the clock (and does freeze time.monotonic()), but it's slow to start; see replace freezegun with time-machine.

Keep the tests correct

  • Polling keeps the test meaningful: it still checks that the thing happens, just without the fixed wait. Keep the timeout well above the normal duration so slow CI machines don't fail.
  • A mocked sleep in retry code should still assert how often the code retried (and ideally with which delays). Otherwise a bug that retries forever, or never, goes unnoticed.
  • When you set a delay to zero in test settings, keep one test that checks the real backoff schedule, with the sleep mocked and its arguments asserted.

How flakytest finds it

flakytest puts samples inside time.sleep() and asyncio.sleep() (any line that calls sleep(, in your code or a library's) in the sleep category. When sleeping takes more than 0.2 seconds and 1% of test time, it suggests Tests are sleeping, with the tests that sleep the most and the lines of your code that lead to the sleep. That tells you whether the sleep is in the test itself or deep in a retry helper. After the fix, flakytest compare shows how much faster the affected tests got.

Further reading

Related guides

Updated October 18, 2026 · Suggest a change