FlawPilot
From the blog

Parallel Testing and Sharding: Running 500 Tests in Under 3 Minutes

A pull request sits waiting on CI. The suite has 500 tests, each one reasonable on its own, a few seconds to load a page, fill a form, check an assertion. Run one at a time, in sequence, and that…

Palak BhanushaliSecurity research28 Oct 20267 min read

A pull request sits waiting on CI. The suite has 500 tests, each one reasonable on its own, a few seconds to load a page, fill a form, check an assertion. Run one at a time, in sequence, and that adds up fast: 40 minutes later, the pipeline finally goes green, and the actual code review hasn't even started yet. Nobody's test was slow. There were just a lot of them, run one after another, waiting on each other for no real reason.

Most of that 500-test suite has no dependency on the test before it. A login test and a checkout test don't need to happen in order, they just happened to be written in that order. Running them in parallel instead of in sequence is usually the single biggest lever for suite speed, and it's also the fastest way to introduce test failures that have nothing to do with the code being tested.

Two Different Kinds of Parallelism

"Parallel testing" usually means one of two things, and they solve different bottlenecks.

Workers run multiple tests at once on a single machine. Playwright, for example, spins up a configurable number of worker processes, each one an independent browser context, and distributes test files across them. A suite that would take 40 minutes sequentially might take 8 minutes across 5 workers on the same CI runner, limited mostly by how many browser instances that machine can actually run at once without starving on CPU or memory.

Sharding splits the suite across multiple separate machines entirely. Instead of one CI runner working through all 500 tests with 5 workers, four runners each take roughly 125 tests, each running its own set of workers. The orchestration layer (GitHub Actions matrix, GitLab CI parallel jobs, or similar) spins up the runners, Playwright's --shard=1/4 flag tells each one which slice of the suite to execute, and the results get merged into a single report afterward.

Used together, they compound: workers cut time within a machine, sharding cuts time by throwing more machines at the problem. That combination is usually what gets a suite from 40 minutes down to the 2-3 minute range for suites in the hundreds of tests.

Why This Breaks Suites That Weren't Built for It

None of this is free. Parallel execution exposes every assumption a test suite was quietly making about running alone.

Shared test data becomes a race condition. If two tests both create a user with the same hardcoded email, or both modify the same seeded row in a test database, running them in sequence never surfaces the conflict, since one finishes before the other starts. Running them as workers at the same time means whichever test runs second might see state the first test left behind, or crash outright on a uniqueness constraint. The fix is usually generating unique data per test (a UUID in the email, a dynamically created record) rather than relying on fixed fixtures.

Global state leaks between tests. A test that sets an environment variable, flips a feature flag through a shared config file, or writes to a shared in-memory store can bleed into whatever else happens to be running on the same worker at the same time. Playwright isolates browser contexts by default, but anything outside the browser, a shared backend, a shared queue, a shared cache, isn't automatically isolated just because the test framework is.

Order-dependent tests stop working entirely. A suite that quietly relied on test A running before test B, because A happened to create a record B then read, falls apart the moment sharding puts A and B on different machines with no shared execution order at all. This is usually a sign the two tests should never have been dependent on each other in the first place, dependency between tests is a design smell regardless of whether the suite runs in parallel.

Rate limits and shared external resources get hit harder. Five workers across four shards means up to twenty tests could be hitting the same third-party sandbox API, the same shared staging database, or the same email-testing inbox simultaneously. A single-threaded suite never stressed that path; a parallelized one can trigger rate limiting or connection pool exhaustion that looks like a flaky test but is actually a capacity problem in the test environment.

What Makes a Suite Safe to Parallelize

Each test needs to be able to run in any order, on any worker, with no assumption about what ran before it. In practice that means: generate unique test data instead of reusing fixed records, avoid tests that depend on side effects from other tests, and keep anything stateful (a database row, a feature flag, a file on disk) scoped to the test that created it and cleaned up afterward, not left for the next test to either rely on or collide with.

It also means being honest about which tests genuinely can't be parallelized. A test that intentionally exercises a global rate limit, or verifies behavior that depends on being the only active session, might need to run in isolation on purpose. That's a reasonable exception as long as it's deliberate, tagged and excluded from the parallel run, rather than an accidental dependency nobody noticed until sharding exposed it.

Setting It Up

A Playwright config enables worker-level parallelism directly:

// playwright.config.ts
export default defineConfig({
  fullyParallel: true,
  workers: process.env.CI ? 4 : undefined,
  retries: process.env.CI ? 1 : 0,
});

Sharding is handled at the CI layer, splitting the playwright test invocation itself:

# .github/workflows/tests.yml
strategy:
  matrix:
    shard: [1, 2, 3, 4]
steps:
  - run: npx playwright test --shard=${{ matrix.shard }}/4

Each of the four matrix jobs runs independently, each with its own set of workers, and most CI providers can merge the resulting reports into one combined view afterward.

Retries Are Not a Substitute for Isolation

A common response to intermittent parallel-run failures is adding retries, rerun a failed test once or twice and let it pass on a second attempt. Retries are a reasonable safety net for genuine environmental flakiness, a slow network blip, a third-party service hiccupping, but they're a bad fix for an actual isolation bug. A test that fails under parallel load because it collided with another test's data will often pass on retry simply because the timing happened to be different that time, which hides the underlying race condition instead of fixing it. If a specific test only fails when run in parallel and passes reliably alone, that's a signal worth investigating directly, not a case for more retries.

Frequently asked questions

Workers run multiple tests in parallel on one machine. Sharding splits the suite across multiple separate machines, each running its own workers. They're usually combined for maximum speed.

Final Thoughts

A 40-minute suite and a 3-minute suite can contain the exact same 500 tests. The difference isn't the tests, it's whether they were written to assume they're running alone. Parallel execution doesn't make a bad test suite faster, it makes every hidden assumption in that suite visible, usually all at once, usually as a wave of confusing failures the first time someone turns workers on.

Treated as a design constraint from the start, not something bolted on later, parallelism is mostly free speed. Treated as an afterthought, it's a reliable way to turn a slow but trustworthy suite into a fast one nobody trusts.

How FlawPilot helps

FlawPilot finds security and quality issues in your AI-built app and shows you how to fix them. It checks your deployed site across security, performance, infrastructure, and SEO, and scans your source code for vulnerabilities, hardcoded secrets, and vulnerable dependencies.

Every finding is prioritized and explained in plain English, with the actual fix: the configuration change, DNS record, security header, or code change needed. For supported findings, AI-powered guidance adds step-by-step instructions and suggested code fixes.

Connect your Git provider to scan your repository alongside your live site, so application findings, code vulnerabilities, secrets, and dependency issues all land in one place.

It fits your existing workflow too: a REST API for scores and findings, an embeddable security badge, and an MCP server so tools like Claude, Cursor, or ChatGPT can read your findings and help you work through them.

The boundaries are clear: the public website scan reads only publicly accessible signals, with no agent or credentials required, and source-code scanning is opt-in and read-only. Fixes are never applied or merged without your review.

Test AutomationParallel TestingCI/CDPlaywrightShardingTest Isolation

Verify your AI-generated app is production-ready.

117 security checks in 60 seconds - free, no account needed.

Scan one page

Enter a URL - no account, no install.

Run a Site Health check

Requires a free account

Crawls every page we can reach and scores each one, so a slow template deep in the site stops hiding behind a healthy homepage.

Scan your source code

Requires a free account

Connect a Git provider to check for vulnerabilities, secrets, and risky dependencies.

Featured on