Scheduling¶
How tests are distributed across workers in the default (--dist load)
mode.
Collection and verification¶
Every worker collects the identical session (same args, same ini, same conftest semantics). Workers verify agreement by item count + hash of the node-id list; one designated worker ships the full list. Divergent collections (typically a randomizing plugin without a fixed seed) abort the run before any misassignment.
Seeding is barrier-free: each worker starts receiving work the moment its own collection verifies against the reference — early collectors run tests while stragglers finish collecting. The refusal guarantee is per-worker: no worker is ever ASSIGNED work before its collection has been cross-checked, so a divergent straggler aborts the run without having received (or misrun) a single test — but tests on already-verified workers may have started by then.
Dispatch order¶
- Long-poles first. Tests with a cached duration ≥ 1s dispatch first, longest first, one at a time — so they spread across workers instead of stacking. This is what beats file-affinity schedulers on wait-heavy suites: a 54-second test starting at t=0 instead of t=90 changes the whole run's wall time.
- Everything else in contiguous chunks. Chunks preserve module locality (module/class fixtures set up once per worker visit) and cut protocol round-trips. Chunk size scales with suite size; refills happen when a worker half-drains.
The duration cache (.rstest_cache/durations.json) is written after every
run, so the first run is collection-ordered and every later run is
duration-aware.
The nextitem invariant¶
pytest's teardown scoping depends on knowing each test's successor
(nextitem): a worker therefore never runs its last pending item until it
learns what comes next — or learns the queue is exhausted for now
(no_more_items, which runs the held item with nextitem=None). Workers
then keep listening: a failed test from any worker can be rerun on them
until an explicit end-of-session signal confirms every outcome is final.
Every dispatch path must keep at least one successor in flight or
explicitly release the queue; this invariant shaped most of the
scheduler's edge cases (three deadlocks' worth).
The serial phase¶
@pytest.mark.serial items are excluded from the parallel queue. One
designated worker is held open; when every other worker's session has
fully finished (fixtures torn down, ports released), the serial items run
there exclusively, in collection order.
Affinity modes¶
--dist loadfile, loadscope, and loadgroup replace the above with
keyed groups in collection order — a dispatch never splits a group, and
duration reordering is off (affinity is the point, at the cost of
long-pole splitting):
loadfile: groups are whole files.loadscope: groups are fixture scopes — a class's tests, or a module's functions.loadgroup: groups are@pytest.mark.xdist_group("name")marks, consolidated across files; unmarked tests stay individual.
Broadcast mode (--dist each)¶
--dist each is not distribution at all — every worker runs the full
suite (xdist --dist=each), so the run legitimately contains each test N
times. It is for multi-environment validation: run the same suite across N
workers configured differently. There is no item dispatch queue; each worker
is seeded with every index, and a crash replacement reruns only the dead
worker's remaining items.
Consequences:
- Outcomes are keyed
nodeid [gwN], since the same test appears once per worker. - The duration cache is not written — N× runs would poison LPT scheduling on the next normal run.
--rerunsis rejected: every worker already runs the suite, so a rerun has no distinct meaning.