iTestResults

Test Duration Variance Reveals More Than Failure Rate

Most teams treat test results like a checkbox: green is good, red is bad, ship or block. The interesting signal lives in everything that happens between those two states — runtime variance, retry counts, the same five tests appearing in every postmortem. Failure rate is a lagging indicator. By the time your pass rate drops, the instability has been compounding for days. Duration variance catches it earlier.

A test that passes in 800 ms on Monday and 4,200 ms on Thursday hasn't failed yet — but it's telling you something is wrong. Resource contention, a slow external dependency, a data fixture that's grown by two orders of magnitude: all of these show up in runtime before they show up in a red build. Treating duration as a first-class signal is the difference between reactive triage and proactive stability work.

By the end of this article you'll know how to model duration variance in SQL, surface it in Grafana, and write a Python analyzer that flags tests whose coefficient of variation exceeds a threshold — before anyone files a bug.

API Testing using Python, Behave, VS Code & GitHub Copilot

Smarter API Test Automation — Python, Behave, VS Code, AI with GitHub Copilot & CI/CD Pipelines. Complete in a Weekend!

Learn more

What Duration Variance Actually Measures in a Test Suite

Duration variance is the statistical spread of a test's execution time across multiple runs. The most useful single-number representation is the coefficient of variation (CV) — standard deviation divided by the mean — because it normalizes across tests that naturally run at different timescales. A 500 ms stddev means something very different for a 200 ms unit test versus a 30-second integration test. CV makes them comparable. A CV above 0.4 on a test that was stable last sprint is a concrete signal worth investigating, not a noise artifact.

In a modern test architecture, duration data lives in your result store — PostgreSQL, ClickHouse, or BigQuery depending on your ingestion volume — and is emitted alongside pass/fail status in JUnit XML, Allure JSON, or OpenTelemetry spans. The problem is that most dashboards discard it. Allure renders a timeline per run; it doesn't track CV across 30 runs. ReportPortal surfaces per-launch statistics but doesn't natively alert on cross-launch variance drift. That gap is where custom analytics earns its keep. As a foundation, your result storage schema needs a duration_ms column indexed alongside test_name and run_timestamp — without that, every query becomes a full scan.

Building a Duration Variance Pipeline: SQL, Python, and Grafana

Start at the query layer. The following PostgreSQL query computes CV per test over a rolling 14-day window and surfaces the top offenders:

SELECT
  test_name,
  COUNT(*)                                      AS run_count,
  ROUND(AVG(duration_ms)::numeric, 1)           AS mean_ms,
  ROUND(STDDEV(duration_ms)::numeric, 1)        AS stddev_ms,
  ROUND((STDDEV(duration_ms) / NULLIF(AVG(duration_ms), 0))::numeric, 3) AS cv,
  ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY duration_ms)::numeric, 1) AS p95_ms
FROM test_results
WHERE run_timestamp >= NOW() - INTERVAL '14 days'
GROUP BY test_name
HAVING COUNT(*) >= 10          -- ignore rarely-run tests
   AND STDDEV(duration_ms) / NULLIF(AVG(duration_ms), 0) > 0.4
ORDER BY cv DESC
LIMIT 50;

The HAVING clause is doing real work here: fewer than 10 runs gives you a CV that's noise, not signal. Plug this into a Grafana table panel backed by your PostgreSQL datasource and you have a live instability leaderboard that refreshes every 15 minutes — no manual triage required.

For automated alerting, a lightweight Python script run as a GitHub Actions scheduled job closes the loop:

import psycopg2, os, json, urllib.request

THRESHOLD_CV = 0.4
conn = psycopg2.connect(os.environ["DB_DSN"])
cur = conn.cursor()
cur.execute("""
    SELECT test_name,
           ROUND((STDDEV(duration_ms) / NULLIF(AVG(duration_ms),0))::numeric,3) AS cv
    FROM test_results
    WHERE run_timestamp >= NOW() - INTERVAL '7 days'
    GROUP BY test_name
    HAVING COUNT(*) >= 10
       AND STDDEV(duration_ms) / NULLIF(AVG(duration_ms),0) > %s
    ORDER BY cv DESC LIMIT 20
""", (THRESHOLD_CV,))
rows = cur.fetchall()

if rows:
    blocks = [{"type":"section","text":{"type":"mrkdwn",
        "text": f"*High duration variance tests (CV > {THRESHOLD_CV})*\n" +
                "\n".join(f"• `{r[0]}` — CV {r[1]}" for r in rows)}}]
    payload = json.dumps({"blocks": blocks}).encode()
    req = urllib.request.Request(os.environ["SLACK_WEBHOOK"],
                                 data=payload,
                                 headers={"Content-Type":"application/json"})
    urllib.request.urlopen(req)

Wire this into a schedule: cron: '0 8 * * 1-5' GitHub Actions workflow and your team gets a Monday-through-Friday Slack digest of the tests most likely to cause a surprise block before they actually fail. One team running this against a Playwright + Pytest suite with ~2,400 tests reduced mean triage time from 22 minutes per failure to under 4 once the Slack digest was cross-linked to Grafana and Loki log correlation — engineers arrived at the right log line without hunting.

For the Grafana side, a time-series panel tracking P95 duration alongside mean duration for a single test is more actionable than a single CV number. When P95 diverges from the mean by more than 2×, you're looking at intermittent slowdowns, not a uniformly slow test. That distinction matters for root-cause: uniform slowness points to a fixture or dependency change; high P95 with stable mean points to resource contention or network jitter — classic flakiness behavior that aggregate averages obscure.

Where Engineers Go Wrong When Tracking Test Duration

The most common mistake is measuring duration at the suite level instead of the test level. Total suite runtime trending up is useful for capacity planning, but it tells you nothing about which specific test is destabilizing. A single test ballooning from 400 ms to 8 seconds in a 600-test suite barely moves the aggregate needle — yet it's the one that will eventually time out and burn your on-call rotation. The org-level reason this happens: dashboards get built by whoever has Grafana access, and suite-level metrics are easier to instrument than per-test metrics in most CI reporters.

The second mistake is not controlling for environment before declaring variance meaningful. A test that runs on a shared Jenkins agent will have higher CV than the same test on a dedicated Buildkite runner with reserved CPU — not because the test is unstable, but because the infrastructure is. Segment your variance analysis by runner type or agent label before escalating. In ClickHouse or BigQuery you can add a runner_class dimension to the GROUP BY for almost no extra cost. Skipping this step generates false positives that erode trust in the dashboard within two weeks of launch.

Myths About Test Failure Analysis That Duration Data Disproves

Myth 1: Pass/fail rate is the primary signal for suite health. A 99% pass rate on a suite where 40 tests have CV > 0.6 is not a healthy suite — it's a suite that hasn't failed yet. Pass rate is the output; variance is an input to that future output. Teams that optimize only for pass rate end up surprised by cascading failures in prod-like environments. This is the same problem as treating a 100% pass rate as meaningful quality evidence — the metric looks clean right up until it doesn't.

Myth 2: Flakiness means random failure, so duration is irrelevant to flake detection. In practice, most flaky tests are slow before they're failing. The test flakiness meaning that matters operationally isn't "sometimes red, sometimes green" — it's "behaviorally inconsistent under the same inputs." Duration inconsistency is behavioral inconsistency. A Selenium or Playwright test that passes in 3 s on one run and 18 s on the next is exhibiting flaky behavior even if both runs are green. Catching it at the duration stage, before the timeout threshold is crossed, is strictly better than catching it at the failure stage. Myth 3: Fixing flakiness requires rewriting the test. Often a CV spike traces back to a single infrastructure change — a new shared fixture, a bumped dependency, a database that wasn't vacuumed. The test is fine; the environment changed.

Duration variance is a first-class engineering signal, not a nice-to-have annotation. Start with the CV query above against your existing result store, set a CV > 0.4 alert in Grafana or Slack, and run it for two weeks. You'll find instability that your failure rate dashboard has been hiding. From there, correlating variance spikes with deployment events and infrastructure changes is the natural next step — the real-time test pipeline observability patterns cover exactly that wiring.

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