Order Dependency: How It Poisons Your Suite

Most flakiness investigations start with the wrong question: "Why did this test fail?" The more dangerous question is "Why did these tests fail together?" When you see a cluster of five unrelated tests going red on the same run — tests that pass in isolation, pass in reruns, and have no obvious shared code path — order dependency is almost always the culprit. The signal isn't in any individual failure; it's in the co-failure pattern.

Order-dependent tests are tests that silently rely on side effects produced by earlier tests in the same run: a database row left behind, a singleton mutated in memory, a temp file not cleaned up, a Playwright browser context that wasn't fully torn down. They pass when the suite runs in one sequence and fail in another, which makes them nearly invisible to standard flakiness detection that only tracks per-test failure rate.

By the end of this article you'll have a detection query, a Python analyzer, and a CI strategy that surfaces order dependencies before they metastasize into suite-wide noise — and you'll understand why most teams' dashboards actively hide this class of problem.

Discover How the Systems Around You Really Work

Understand the government, financial, healthcare, business, and technology systems affecting everyday life.

Learn more

What Order Dependency Actually Is (and Isn't)

An order-dependent test is not simply a flaky test. A flaky test fails non-deterministically due to timing, network, or resource contention. An order-dependent test fails deterministically given the same execution sequence — it's just that the sequence isn't encoded anywhere visible. This distinction matters because the fix paths are completely different: flakiness mitigation (retries, quarantine) does nothing for order dependency and can actually mask it by making the offending test appear to pass on retry when the state happens to reset.

In a modern test architecture, order dependency typically lives at three layers: shared database state (tests using the same schema without transaction rollback), in-process global state (module-level caches, class-level fixtures in Pytest, static fields in JUnit), and external resource state (S3 buckets, Redis keys, feature flags toggled by one test and read by another). Each layer has different detection and remediation strategies. The common thread is that isolation was assumed but never enforced — and no CI gate caught the gap.

Detecting and Eliminating Execution-Order Coupling

The first step is making order visible in your test result store. If you're persisting JUnit XML into PostgreSQL or ClickHouse, you already have the run ID, test name, and start timestamp. Add an execution_index column populated at ingest time, then query for tests whose pass/fail outcome correlates with their position in the run:

-- PostgreSQL: find tests whose failure rate spikes in the first 20% of a run
SELECT
  test_name,
  ROUND(AVG(CASE WHEN execution_index::float / suite_size < 0.2 AND status = 'failed' THEN 1 ELSE 0 END), 3) AS early_fail_rate,
  ROUND(AVG(CASE WHEN execution_index::float / suite_size >= 0.2 AND status = 'failed' THEN 1 ELSE 0 END), 3) AS late_fail_rate,
  COUNT(*) AS total_runs
FROM test_results
WHERE run_date >= NOW() - INTERVAL '30 days'
GROUP BY test_name
HAVING COUNT(*) > 50
  AND ABS(
    AVG(CASE WHEN execution_index::float / suite_size < 0.2 THEN CASE WHEN status='failed' THEN 1 ELSE 0 END END) -
    AVG(CASE WHEN execution_index::float / suite_size >= 0.2 THEN CASE WHEN status='failed' THEN 1 ELSE 0 END END)
  ) > 0.15
ORDER BY ABS(early_fail_rate - late_fail_rate) DESC;

A delta greater than 0.15 between position buckets is a strong signal of order coupling, not random flakiness. Once you have candidates, use pytest-randomly with a fixed seed to reproduce the failure, then bisect with pytest --randomly-seed=last -p no:randomly to pin the sequence. For JUnit-based suites, how suite size distorts your aggregate failure rate is worth understanding before you interpret these numbers — larger suites dilute the per-test signal.

For automated detection in CI, a Python script that re-runs failing tests in reverse order is more actionable than any dashboard:

import subprocess, sys, json

def rerun_reversed(test_ids: list[str], seed: int) -> dict:
    """Re-run a subset in reversed order; returns {test_id: status}."""
    reversed_ids = list(reversed(test_ids))
    result = subprocess.run(
        ["pytest", "--no-header", "-q", "--tb=no",
         f"--randomly-seed={seed}", "--collect-only", "-q"] + reversed_ids,
        capture_output=True, text=True
    )
    # Parse collected order, then run and capture outcomes
    run = subprocess.run(
        ["pytest", "-p", "no:randomly", "--tb=short", "-q",
         "--json-report", "--json-report-file=rerun.json"] + reversed_ids,
        capture_output=True, text=True
    )
    with open("rerun.json") as f:
        report = json.load(f)
    return {t["nodeid"]: t["outcome"] for t in report["tests"]}

Wire this into your GitHub Actions failure handler as a post-step: when a test job exits non-zero, extract the failing node IDs from the JUnit XML artifact, invoke the reversal script, and post the diff to the PR as a check annotation. One team reduced triage time from 22 minutes per failure cluster to under 4 minutes once the annotation surfaced "fails only when preceded by test_create_user" directly in the PR review UI. For deeper causality tracing — especially when the pollution crosses service boundaries — distributed tracing tied to test execution spans gives you the full picture that log scraping alone won't.

Where Senior Engineers Still Get Burned

The most common mistake is quarantining the victim, not the polluter. When test_checkout_flow goes red intermittently, it gets tagged flaky and moved to a non-blocking suite. But the actual culprit — test_apply_discount, which leaves a coupon code in the shared database — keeps running, keeps polluting, and eventually causes a different victim to surface. You've now quarantined two tests and fixed zero problems. The fix is to identify the polluter via the co-failure graph, not the test that shows the symptom.

The second mistake is trusting parallel execution to break order dependency. Teams split their suite across 8 workers in CircleCI or Buildkite and assume parallelism randomizes order sufficiently. It doesn't — each worker runs its own ordered shard, and if your test splitter groups by file (the default in most runners), all tests in a file still run in declaration order. Shard-level isolation is not the same as test-level isolation. You need explicit randomization within each worker, not just across workers, and you need database-per-worker or transaction rollback to enforce state isolation at the right boundary.

Myths That Let Order Dependency Hide in Plain Sight

Myth 1: "Our tests are isolated because we use fixtures." Pytest fixtures with scope="module" or scope="session" are shared state by design. A session-scoped database fixture that doesn't roll back between tests is an order dependency factory. The fix isn't removing fixtures — it's auditing scope and enforcing rollback or teardown at the right level. Myth 2: "Passing in isolation means the test is clean." Isolation reruns only tell you the test doesn't depend on prior state; they don't tell you whether it produces state that breaks something downstream. The polluter almost always passes in isolation. This is why AI failure classifiers misread cascading failures — they see clean individual signals and miss the causal chain.

Myth 3: "A green suite means no order dependency." If your suite always runs in the same order — which is true of any CI pipeline that doesn't explicitly randomize — order-dependent tests are permanently hidden. They only surface when someone adds a new test above the polluter, reorders a file, or enables parallelism for the first time. By then the suite has accumulated years of implicit ordering contracts that nobody documented. The operational fix is to add pytest-randomly (or equivalent) to your standard CI run, not just a nightly audit job, so order assumptions fail fast rather than accumulating silently. Also worth noting: suite composition skews the metrics you report in ways that make this class of problem appear less severe than it is.

Order dependency is a structural problem, not a test-quality problem — it's a missing contract between tests and their environment. Start with the co-failure query above against your last 30 days of results; most teams find 3–8 high-confidence candidates on the first run. Fix the polluters, enforce transaction rollback or per-test database isolation at the fixture layer, and add randomized ordering to your standard CI matrix. That's the full loop: detect, attribute, isolate, prevent.

Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.

Understanding how systems actually work is the first step toward navigating them effectively.

Browse all articles