Scaling Browser Tests Without Breaking Things

2026-03-27 · SPUNK13 · spunk.bet

A browser suite gets slow and flaky at roughly the same point — somewhere around 200 tests, when the runtime crosses fifteen minutes and people start re-running failures instead of reading them. Both problems have the same root cause and largely the same fixes.

Shard Across Machines, Parallelise Within Them

Playwright splits work two ways and you want both. Inside one machine, workers controls parallel browser contexts; on CI, one worker per available core is a sane default and more than that usually makes things slower because each Chromium context wants 150–300 MB of RAM. Across machines, use --shard=1/4 through --shard=4/4 in a CI matrix and merge the reports afterwards:

npx playwright test --shard=${{ matrix.shard }}/4 --reporter=blob
npx playwright merge-reports --reporter=html ./blob-report

Four shards on four runners turns a 16-minute suite into roughly 4 minutes plus about 40 seconds of container startup. The ceiling is set by your slowest single test, so a 3-minute end-to-end checkout test caps how far sharding can take you.

Never Sleep, Always Assert

The single largest source of flake is waitForTimeout. Fixed sleeps are either too short on a loaded CI runner or wasted time on a fast one. Playwright's locators auto-wait on actionability, and web-first assertions retry until the timeout:

// flaky
await page.waitForTimeout(2000);
expect(await page.locator('.total').textContent()).toBe('$42.00');

// deterministic
await expect(page.getByTestId('cart-total')).toHaveText('$42.00');

Grep your repo for waitForTimeout and treat every hit as a bug. The same applies to CSS selectors that encode styling — .btn-primary.mt-4 breaks on a design tweak. Prefer getByRole, getByLabel and explicit data-testid attributes, in that order.

Stop Logging In Over and Over

If every test walks the login form, you are spending several seconds per test on the same three requests. Log in once in a global setup, save cookies and local storage with context.storageState({ path: 'auth.json' }), and load it via the storageState option in your project config. On a 200-test suite this routinely removes several minutes. Keep one real login test so you still notice if authentication itself breaks.

Isolate Test Data

Tests that share a fixture account will collide as soon as they run in parallel. Create data per test with a unique key — a UUID or the worker index — and clean up in an afterEach. If your app supports it, seed through the API rather than the UI; a POST that sets up an order in 80 ms is worth more than a UI flow that takes 12 seconds and fails for reasons unrelated to the test's purpose.

Quarantine Rather Than Retry Blindly

retries: 2 in CI hides real bugs while making the suite three times slower on a bad day. A better policy: retries on, but every test that passes only on retry gets tagged and reported. Playwright marks these as "flaky" in the HTML report. Set a budget — for example, no more than 1 percent flaky — and fix or delete anything above it. A test nobody trusts is worse than no test, because it trains the team to ignore red.

Use the Trace Viewer Instead of Guessing

Set trace: 'on-first-retry'. When something fails in CI you get a zip containing a DOM snapshot per action, network log, console output and a screenshot timeline; npx playwright show-trace trace.zip opens it locally. This converts "cannot reproduce" into a two-minute diagnosis and is the highest-value config line in the whole file.

Know What Not to Test in a Browser

Browser tests are the slowest and most fragile layer you own. Validation rules, formatting, currency maths and permission logic belong in unit tests that run in milliseconds. Keep the browser suite for the handful of journeys that would cost you money if they broke — sign-up, checkout, and whatever your support inbox complains about most.

Explore More

Free tools, guides, and resources.

Visit spunk.bet
400+ ToolsCasinoMemesAstrologyScam DB