Skip to content
End To End Tester

Running Tests in Parallel

How parallelism turns hidden coupling into failures, the shared resources that clash, and how to make a suite genuinely safe to run concurrently.

2 min read · updated 19 September 2026

Parallelism is the cheapest speed-up available and the fastest way to discover that your tests were never independent.

The failures it produces are not new. They were always latent — two tests sharing an account, a fixed port, a static cache — and running in sequence merely hid them.

#What clashes

Database rows. Two tests creating [email protected]. One passes, one gets a unique-constraint violation, and which one depends on scheduling.

Fixed ports. A stub server on :8080. The second worker cannot bind.

Files and directories. /tmp/report.pdf, ./downloads/, screenshots/latest.png. Last writer wins and the assertion reads someone else's output.

Shared accounts. Every test signs in as [email protected]; one changes the password; the rest fail.

In-process singletons. A static cache, a module-level config, a connection pool holding a transaction. Worst of the lot because it is invisible.

External rate limits. Eight workers hitting a sandbox API that allows five requests a second.

The clock. One test freezes time globally. Everything else concurrently running now lives in 1970.

#The fixes, in order of value

#1. Unique data by construction

Never a literal in a unique column. Every email, reference, tenant and filename carries a UUID or the worker index. This one habit removes most clashes.

typescript
// Playwright exposes the worker index; every runner has an equivalent.
const email = `buyer-${test.info().workerIndex}-${Date.now()}@example.test`;
csharp
// xUnit: collections run in parallel, so make the key unique per test.
var reference = $"ORD-{Guid.NewGuid():N}";

See test data management.

#2. Ephemeral ports, always

typescript
const server = app.listen(0);                    // 0 = the OS picks a free one
const { port } = server.address() as AddressInfo;
java
var wireMock = new WireMockServer(options().dynamicPort());

A hardcoded port is the single most common reason a suite cannot be parallelised.

#3. A scope per worker

Give each worker its own database, schema or tenant. With Testcontainers this is a few lines:

typescript
// One Postgres per worker, started once and reused across that worker's tests.
export const test = base.extend<{}, { db: StartedPostgreSqlContainer }>({
  db: [async ({}, use) => {
    const container = await new PostgreSqlContainer('postgres:16-alpine').start();
    await migrate(container.getConnectionUri());
    await use(container);
    await container.stop();
  }, { scope: 'worker' }]
});
python
# pytest-xdist: each worker gets its own schema, derived from its id.
@pytest.fixture(scope="session")
def schema(worker_id, engine):
    name = f"test_{worker_id}"          # 'gw0', 'gw1', ... or 'master'
    engine.execute(f'CREATE SCHEMA IF NOT EXISTS "{name}"')
    return name

#4. Temporary directories per test

python
def test_writes_a_report(tmp_path):       # pytest gives each test its own
    write_report(tmp_path / "report.pdf")
    assert (tmp_path / "report.pdf").exists()

#5. No module-level mutable state

If a singleton must exist, give it a reset hook and call it in beforeEach. Better: make it an instance and inject it.

#Turning it on

typescript
// playwright.config.ts
export default defineConfig({
  fullyParallel: true,                        // parallel within files too
  workers: process.env.CI ? 4 : undefined,    // undefined = half the cores
  forbidOnly: !!process.env.CI                // .only must not reach CI
});
csharp
// xUnit: classes run in parallel by default. Opt a class out when it
// genuinely cannot share — and treat needing this as a smell.
[Collection("Sequential")]
public class MigrationTests { }
python
# pytest-xdist
pytest -n auto --dist loadfile     # loadfile keeps a file's tests on one worker
java
// JUnit 5 — junit-platform.properties
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent

#Finding the coupling you have

Randomise order. Most runners can shuffle. A suite that fails under shuffle has an order dependency, and finding it now is cheaper than finding it in a shard.

bash
pytest -p no:randomly --randomly-seed=12345
npx jest --randomize
dotnet test -- xunit.execution.DisableParallelization=false

Run the suite twice in the same container. Anything that fails the second time is leaving state behind.

Run one test a hundred times concurrently. Isolates genuine races from cross-test interference.

#The relationship to flakiness and sharding

Parallelism does not cause flaky tests; it converts deterministic hidden coupling into non-deterministic visible failure. That feels worse and is strictly better: the bug was always there and now you can see it.

Get the suite parallel-safe first, then shard it across machines. Sharding an unsafe suite multiplies the flakiness by the number of machines and produces a pipeline nobody trusts.

Common questions

Why do my tests pass individually but fail when run in parallel?
Shared state. Two tests are using the same database row, the same file path, the same port, the same user account or the same in-process singleton. Parallelism does not create the coupling — it reveals coupling that was always there and happened to be harmless in sequence.
What is the difference between parallelism and sharding?
Parallelism runs several tests at once on one machine, across processes or threads. Sharding splits the suite across several machines, each running its own subset. They compose — eight machines running four workers each is thirty-two concurrent tests.
How many workers should I use?
Start at the number of CPU cores and measure. Browser tests are memory-hungry, so the practical limit is often RAM rather than CPU; CI runners with two cores frequently do best at two workers regardless of what the machine reports.

Runnable samples for this page

last test results ↗
  • TypeScripttypescript/src/practices/parallel-test-execution

Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.

Was this page useful?