July 28, 2026
How to Benchmark Flake Rates in Browser Suites Before and After CI Runner Changes
A practical browser test flake rate benchmark plan for comparing CI runner changes, isolating infrastructure noise, and protecting regression signal quality in browser suites.
When a browser suite starts behaving differently after a CI change, the first question is usually the wrong one. Teams ask, “Did the tests get worse?” when the more useful question is, “What changed in the execution environment, and how much of the variance is now coming from there?” That distinction matters because a test that fails intermittently for infrastructure reasons can look exactly like a product regression until you separate the signal from the noise.
A good browser test flake rate benchmark gives you a way to compare suites before and after changes to CI runners, images, browser versions, CPU allocation, disk behavior, or network shape. The point is not to chase a single magic number. The point is to measure whether your change moved the failure distribution, altered retry behavior, or changed the quality of the signal you get from test failures.
This article lays out a practical benchmark plan for QA engineers, DevOps, SREs, and test managers who need to answer a boring but expensive question: did the runner change introduce more flakiness, or did it just expose flakiness that was already there?
What you are actually benchmarking
A browser test flake rate benchmark should separate at least four things:
- Test logic failures, where the product or the test is genuinely broken.
- Infrastructure-induced noise, where the runner, image, browser, or network makes a healthy test fail.
- Retry behavior, where a retry hides a real problem or inflates the apparent stability of the suite.
- Regression signal quality, meaning how confidently a failing test points to a real issue after the change.
That last one is easy to ignore. A suite can have the same aggregate pass rate before and after a runner change, while still getting worse in practice because failures are less repeatable, slower to diagnose, or more likely to be masked by retries.
The benchmark is therefore not just “how many failures occurred.” It is a small measurement system for comparing failure modes under controlled conditions. That framing is close to the broader definition of software testing and test automation, but the benchmark has a narrower purpose: it measures the reliability of your browser automation execution environment, not just the application under test. For background, the general concepts are summarized in software testing, test automation, and continuous integration.
If a runner change increases failures by 2%, the meaningful question is not whether 2% is “big.” It is whether that 2% is concentrated in a few fragile tests, spread across the suite, or caused by a measurable environment shift that will keep recurring.
Define the benchmark question before you measure anything
A benchmark that starts with raw test results and ends with an argument is usually missing a definition. Before you run the suite, write down the exact comparison you care about.
Typical benchmark questions
- Did the new runner image increase the absolute flake rate?
- Did retries become more common after the change?
- Did failures move from one browser family to another?
- Did suite runtime variance increase enough to affect timeout behavior?
- Did test reruns become less informative because failures are now non-deterministic?
Each of these questions implies a different metric, and mixing them will blur your conclusion. For example, a new runner might reduce wall-clock time but increase timeout-sensitive failures because the CPU limit changed. If you only track pass/fail, you will miss the mechanism.
State the comparison window
Make the time window explicit. Common options are:
- Before/after paired windows of equal length, such as 2 weeks before and 2 weeks after a runner rollout
- Matched build sets, where the same test commit range is run on both environments
- Canary comparisons, where a subset of jobs runs on the new runner alongside the old runner
The most defensible benchmark usually uses matched builds, because it reduces confusion from product changes. If the app under test changed at the same time, you are no longer benchmarking the runner alone.
Choose the right flake rate metric
The phrase “flake rate” gets used loosely. For a browser test flake rate benchmark, you need a definition that can survive scrutiny.
Useful metrics
Run-level flake rate
The percentage of test executions that end in failure but pass when repeated under the same commit and environment class.
This is useful, but only if retries are controlled. A test that passes on retry might still be flaky, or it might be a legitimate signal of eventual consistency.
Test-case flake rate
The percentage of distinct test cases that fail at least once across the benchmark window.
This is often more stable than run-level failure counts, because it avoids over-weighting tests that are executed more frequently.
Retry amplification
The ratio between initial failures and final failures after retries.
If the new runner causes many more first-attempt failures but the final reported failures stay flat, your suite may be masking instability rather than improving.
Variance in runtime and timeout incidence
Changes in median runtime, p95 runtime, and timeout frequency can reveal performance drift that shows up as flakiness later.
A practical definition
For most teams, a good operational definition is:
- A flake is a failure that disappears on immediate rerun under the same commit, same suite, and same environment class.
- The benchmark measures both initial failure rate and final failure rate.
- A separate label tracks known test bugs versus suspected environment-induced noise.
That definition is imperfect, but it is explainable. The main tradeoff is that it depends on reruns. If reruns are slow, expensive, or not automated, you will need a sampling strategy.
Freeze the variables you can control
A benchmark is only as clean as the set of things you hold constant. Browser suites are especially sensitive because many small changes compound.
Control these variables first
- Test commit revision, ideally a fixed commit range or the same merge-base
- Browser version, including patch level, not just major version
- Runner image, operating system, system packages, fonts, and timezone
- CPU and memory limits, including container resource quotas
- Parallelism level, since concurrency often changes timing and order-dependent bugs
- Test data and backend state, especially if tests depend on seeded accounts or shared fixtures
- Network conditions, including proxies, DNS behavior, and external service reachability
If you cannot freeze something, document it and treat it as a possible confounder.
A benchmark that changes runner images, browser versions, and test parallelism at once is not a benchmark. It is a story generator.
Use environment labels
Every run should be tagged with a small set of labels, at minimum:
- runner type
- image version
- browser version
- suite name
- commit SHA
- branch or release train
- retry policy
- parallelism count
Without labels, you cannot compare failure clusters later. In practice, the teams that resolve flake questions fastest are usually the teams that invested early in metadata discipline, not the teams with the fanciest test framework.
Design the benchmark as a paired experiment
The cleanest design is to run the same browser suite in parallel on the old and new CI runners.
Recommended experiment shape
- Pick a stable build window, ideally a set of commits that are not under active churn.
- Run the suite on the current runner and the candidate runner.
- Keep retries identical across both environments.
- Compare not just final failures, but first-pass failures, retries, runtime variance, and timeout incidence.
- Repeat enough times to see whether the result is stable across runs.
This paired approach is better than a blind before/after comparison because product changes, seed data changes, and unrelated infrastructure changes can otherwise distort the results.
If you cannot run both in parallel
If you only have one environment at a time, use a matched historical baseline.
That means selecting a previous period where:
- the suite was similar in size and composition
- the application release rate was similar
- the retry policy was unchanged
- the browser versions were close enough to be comparable
This is weaker evidence than a paired experiment, but still better than a casual look at recent failures.
Measure the failure shape, not just the failure count
Flakiness is not one thing. Different runner changes create different kinds of failure shape.
Common shapes you should look for
Short, noisy failures
These often show up as locator timing issues, animation waits, or network race conditions. The test fails quickly and then passes on retry.
Slow failures
These often look like timeout drift, browser startup lag, or CPU starvation. The test may fail in predictable places after the runner change.
Clustered failures
A small number of tests fail repeatedly. This suggests environment sensitivity in specific flows, rather than broad suite degradation.
Distributed failures
Many tests fail once or twice with no obvious commonality. This usually points to environment instability, shared setup problems, or systemic drift.
Segment by test class
Break the suite into meaningful groups:
- login and auth flows
- navigation and rendering checks
- data mutation flows
- API-backed UI flows
- cross-browser compatibility tests
A runner change may hurt some classes and help others. For example, a faster but more memory-constrained runner might improve lightweight checks while increasing failures in data-heavy end-to-end flows.
Retry variance is part of the benchmark
Retries are a tool, but they are also a measurement hazard. They can hide the very instability you are trying to quantify.
What to record about retries
- number of first attempts
- number of retries used
- whether the retry passed or failed
- time to recovery
- whether the same step failed in the same place on rerun
A useful extra metric is retry variance, which is how much the result changes across repeated executions of the same test under the same commit and runner class.
If a test passes on the first runner, fails on the second, and passes on the third, the suite may be near a timing boundary. The runner change did not invent the problem, but it may have moved the boundary into the dangerous zone.
Avoid unlimited retries
Unlimited retries make the benchmark less trustworthy because they erase evidence. If the objective is to compare flake rates, the retry policy must be fixed and conservative.
A common pattern is:
- one immediate retry for classification
- no more than one or two retries for final reporting
- separate tracking for rerun-only successes
This keeps the benchmark honest about instability while still giving you some signal about transient failures.
A minimal data model for the benchmark
You do not need a data warehouse to start. You do need a structure.
At minimum, capture one row per test execution with fields like:
- run ID
- commit SHA
- environment class
- browser name and version
- test ID
- attempt number
- status
- duration
- failure category
- retry outcome
- timestamp
Example schema:
{ “run_id”: “ci-2026-07-01-0142”, “commit_sha”: “a1b2c3d”, “environment”: “runner-v2”, “browser”: “chromium-127”, “test_id”: “checkout/guest-can-purchase”, “attempt”: 1, “status”: “failed”, “duration_ms”: 18342, “failure_category”: “timeout”, “retry_outcome”: “passed” }
Once you have this, you can compute the core comparisons:
- initial failure rate by environment
- final failure rate by environment
- retry-only pass rate
- timeout rate by environment
- median and p95 test duration by environment
Example: benchmarking with Playwright in CI
Playwright is a reasonable reference point because it makes retries, tracing, and browser selection explicit. The exact framework matters less than the measurement discipline, but the tooling should support per-run metadata and reproducible retries.
A simple Playwright config with one retry and trace collection on first failure might look like this:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({ retries: 1, reporter: [[‘json’, { outputFile: ‘results.json’ }]], use: { trace: ‘on-first-retry’, }, projects: [ { name: ‘chromium’, use: { …devices[‘Desktop Chrome’] } }, ], });
In CI, the important part is not the framework choice, it is that the runner label, browser version, and retry policy are fixed for the comparison.
A GitHub Actions job could separate old and new runners like this:
name: browser-benchmark
on: [workflow_dispatch]
jobs: suite: strategy: matrix: runner: [old, new] runs-on: $ steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ‘20’ - run: npm ci - run: npx playwright test –reporter=json - run: node scripts/summarize-results.js results.json $
The benchmark job should write out a comparable artifact for both environments. If the outputs differ structurally, your comparison will be messy.
How to interpret results without fooling yourself
A runner change can improve one metric while worsening another. That is normal.
Good signs
- Final failures stay flat or decrease, while first-attempt failures also decrease.
- Runtime variance falls, especially at the tail.
- Timeout failures drop without an increase in logic failures.
- Retry-only passes remain rare.
Worrisome signs
- Final failures stay flat, but first-attempt failures rise sharply.
- Retry-only passes increase, which suggests more instability hidden by retries.
- Failures cluster around a specific browser or shard.
- Runtime p95 rises enough that timeouts appear in previously stable flows.
The key judgment call
A runner change that improves infrastructure but exposes hidden test bugs is not necessarily bad. In fact, it may be good if it makes the suite more honest. The real question is whether the new environment creates a stable regression signal or a noisy one.
If the new runner makes failures more reproducible, that can be a net win even if raw flake counts do not fall immediately. If it makes failures less reproducible, the team loses diagnostic quality, which usually costs more than a small performance gain.
Separate infrastructure regressions from test design problems
After you see a change in flake rate, the next job is classification.
Infrastructure-induced noise often looks like
- browser startup failures
- abrupt timeouts across unrelated tests
- failures only on one runner class
- frequent download, disk, or network errors
- memory pressure symptoms, such as crashed tabs or slow page loads
Test design problems often look like
- overly strict selectors
- waits tied to arbitrary sleep values
- assertions that depend on animation timing
- shared state between tests
- tests that assume order or timing that the app does not guarantee
A benchmark does not solve these problems, but it helps you stop blaming the wrong layer.
If failures appear after a runner change, do not assume the suite became worse. It may have simply become more honest about the fragility that was already there.
A simple decision framework for accepting a runner change
Not every measured difference should block rollout. Use a practical decision framework.
Accept the change when
- the final failure rate is stable or improved
- retry amplification does not worsen materially
- runtime variance stays within an acceptable band
- failure clustering does not broaden across the suite
- any new failures are explainable and actionable
Pause or roll back when
- retry-only passes rise sharply
- timeout failures increase in otherwise stable flows
- failure reproducibility drops, making debugging harder
- one browser family regresses disproportionately
- the benchmark cannot distinguish real product issues from infrastructure noise
Mitigate instead of blocking when
- the new runner is clearly better overall, but a small subset of tests needs tuning
- the browser version changed behavior in a known area
- parallelism needs to be reduced or reshaped
- test data setup needs to be isolated more carefully
The point is not to demand perfection. The point is to decide with enough evidence that the change helps more than it hurts.
Operational habits that keep the benchmark useful over time
A one-time benchmark is useful. A repeatable benchmark is better.
Keep the benchmark suite representative
Include tests that reflect real suite behavior, not just the easiest green path. If only fast, isolated checks are measured, runner changes that hurt long flows will slip through.
Re-run the benchmark after environment changes
Treat these as triggers:
- base image rebuilds
- browser upgrades
- kernel or container runtime changes
- network proxy updates
- test parallelism changes
- major CI platform migrations
Preserve historical runs
Retain enough raw execution data to compare later changes against earlier baselines. Summary dashboards are useful, but raw records are what let you diagnose failure shape.
Review benchmark drift itself
The benchmark can become stale. If the test mix changes substantially, the old baseline may no longer be relevant. That is not a failure of benchmarking, it is a reason to recalibrate it.
A practical checklist before and after the runner change
Use this as the minimum discipline for a browser test flake rate benchmark:
- Fix the commit range or use matched builds.
- Hold browser version constant when possible.
- Keep retries and parallelism identical.
- Label every execution with environment metadata.
- Compare first-pass failures and final failures.
- Track retry-only passes separately.
- Segment results by test class and browser.
- Inspect runtime variance and timeout incidence.
- Preserve raw results for later drill-down.
- Decide acceptance based on signal quality, not only pass rate.
Final thought: benchmark the noise before you trust the signal
CI runner changes are attractive because they promise speed, cost relief, or platform standardization. But browser suites are sensitive instruments. If the instrument changes, you have to recalibrate before you trust the readings.
A good browser test flake rate benchmark does not just tell you whether a suite fails more often. It tells you whether the failures are more informative, more reproducible, and more likely to point to the real problem. That is the difference between a useful CI change and a very expensive round of guessing.
If you treat the benchmark as a comparison of failure shape, retry behavior, and environment drift, you will make better rollout decisions and spend less time arguing about whether the suite is “just flaky.” More importantly, you will know when the runner change uncovered a real issue and when it merely changed the weather.