Assertion Latency Masks Slow Dependency Failures

Most teams look at test duration as a performance curiosity, not a failure signal. A test that takes 8 seconds instead of 800ms is still green, so it ships. The problem is that a dependency silently degrading — a database connection pool exhausting, a downstream service hitting its rate limit, a Redis cluster under memory pressure — often shows up as assertion latency long before it shows up as a failure. By the time the test goes red, the dependency has been limping for hours.

The gap between "slow test" and "failing test" is where the real diagnostic signal lives. Standard JUnit XML reports duration at the suite and case level, but they don't tell you whether those 8 seconds were spent in setup, in the system under test, or waiting on a flaky external call that happened to resolve before your assertion timeout fired. That distinction matters enormously when you're triaging a 3am incident.

This article covers how to instrument assertion timing precisely, query that data to surface dependency-correlated slowdowns, and build dashboards that catch slow failures before they become hard failures — without waiting for a red build to tell you something is wrong.

Build Better Test Data for Modern Systems

Learn practical strategies for generating, managing, validating, and scaling reliable test data.

Learn more

What Assertion Latency Actually Measures — and Why It's Different from Test Duration

Test duration is wall-clock time from test start to teardown. Assertion latency is the time elapsed between issuing the action under test and evaluating its result — the window where your test is actively waiting for the system to respond. In a well-instrumented suite, these two numbers diverge precisely when a dependency is slow: setup and teardown stay flat, but assertion latency spikes. That spike is a leading indicator, not a lagging one.

In a modern test architecture, assertion latency sits between your test runner (Pytest, JUnit, Playwright) and your observability layer. It's the measurement you need to correlate with distributed traces — specifically the spans produced by the service under test during that same window. Without it, you're correlating wall-clock test duration against service P95 latency and hoping the timestamps line up. With it, you can join on span IDs and get exact causality. The concept connects directly to how distributed tracing surfaces failure context that JUnit XML simply cannot carry.

Instrumenting, Storing, and Querying Assertion Latency in Practice

Start at the Pytest layer. A simple fixture wraps each assertion block with a timer and emits an OpenTelemetry span, letting you correlate test-side latency with backend traces without changing your test logic:

# conftest.py — requires opentelemetry-sdk >= 1.20
import time, pytest
from opentelemetry import trace

tracer = trace.get_tracer("test.assertions")

@pytest.fixture(autouse=True)
def assertion_span(request):
    with tracer.start_as_current_span(
        "assertion_window",
        attributes={"test.name": request.node.nodeid}
    ) as span:
        start = time.perf_counter()
        yield
        elapsed_ms = (time.perf_counter() - start) * 1000
        span.set_attribute("assertion.latency_ms", elapsed_ms)
        if elapsed_ms > 2000:
            span.set_attribute("assertion.slow", True)

Ship those spans to Honeycomb or your Grafana Tempo instance. The assertion.latency_ms attribute is now queryable alongside every other span attribute — service name, HTTP status, DB query time. Once you have 48 hours of data, the pattern is usually obvious: assertion latency on your checkout tests tracks almost perfectly with your payment-service's P95 DB query time, with a 30-second lag.

For teams storing test results in ClickHouse or BigQuery, the join query is straightforward. Here's a ClickHouse example that surfaces tests where assertion latency exceeded 2× their 7-day median, grouped by the upstream service span that was active during the window:

-- ClickHouse: slow-assertion tests correlated with upstream service
SELECT
    t.test_name,
    t.assertion_latency_ms,
    t.p50_7d,
    s.service_name,
    s.span_duration_ms
FROM test_runs t
JOIN otel_spans s
    ON s.trace_id = t.trace_id
    AND s.start_time BETWEEN t.assertion_start AND t.assertion_end
WHERE t.assertion_latency_ms > 2 * t.p50_7d
  AND t.run_date >= today() - 7
ORDER BY t.assertion_latency_ms DESC
LIMIT 50;

This query alone changed triage velocity on one platform team's suite: triage time dropped from 22 minutes per failure to under 4 once the dashboard was wired to surface the correlated upstream span alongside the test result. The key insight is that the slow span was almost always a PostgreSQL SELECT inside the auth service — invisible from the test report, obvious from the trace join. If your AI-assisted triage pipeline ingests this enriched context, it also avoids the misclassification problem described in how classifiers misread slow-start flakes — the latency spike looks like a regression to a naive model, but the span join reveals it's environmental.

Where Instrumentation Goes Wrong: Three Mistakes That Corrupt the Signal

The most common mistake is measuring total test duration as a proxy for assertion latency. This happens because JUnit XML gives you duration for free, and adding custom instrumentation feels like overhead. The result is a dashboard that shows "slow tests" without distinguishing a slow setup (usually a test isolation problem — see order dependency poisoning your suite) from a slow assertion (usually a dependency problem). The fix is a two-line fixture, not a new tool.

The second mistake is setting assertion timeouts too generously to prevent flakiness. A 30-second timeout on a call that should complete in 400ms means a dependency can degrade by 7,400% before your test fails. You've eliminated the flake but destroyed the signal. Set timeouts at 3–5× the P95 of the healthy baseline, not at "whatever stops the test from failing." The third mistake is not propagating trace context from the test runner into the system under test — without a shared trace_id, the span join in the query above is impossible, and you're back to correlating timestamps and hoping.

Two Assumptions That Keep Teams Blind to Slow Dependency Failures

Myth one: a green test means the dependency is healthy. A green test means the dependency responded within your timeout. If your timeout is 30 seconds and the healthy P95 is 300ms, a green test at 28 seconds is a five-alarm signal dressed as a pass. Teams that track only pass/fail ratios will ship that build and page at 3am when the dependency finally crosses the threshold. The signal you need is assertion latency trend over time, not binary state — exactly the kind of nuance that latency, throughput, and stability metrics are designed to surface together.

Myth two: slow tests are a test-code problem. This assumption sends engineers into conftest refactors and parallelization sprints when the actual fix is a missing database index or an underpowered staging Redis instance. Assertion latency data, joined against infrastructure metrics, makes the causality explicit. Before blaming the test suite, run the ClickHouse query above for a week. If the slow tests cluster around one service and one time window, the test code is not your problem.

Assertion latency is a cheap measurement with disproportionate diagnostic value. Add the OpenTelemetry fixture, store the attribute alongside your test results, and build one dashboard panel that shows latency trend per test correlated with upstream span duration. Run it for two weeks. The dependency that's been quietly degrading will surface itself — before it takes down a build, and long before it takes down production. From there, enriching your triage pipeline with that span context is the logical next step.

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