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.
// Playwright exposes the worker index; every runner has an equivalent.
const email = `buyer-${test.info().workerIndex}-${Date.now()}@example.test`;// 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
const server = app.listen(0); // 0 = the OS picks a free one
const { port } = server.address() as AddressInfo;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:
// 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' }]
});# 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
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
// 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
});// 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 { }# pytest-xdist
pytest -n auto --dist loadfile # loadfile keeps a file's tests on one worker// 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.
pytest -p no:randomly --randomly-seed=12345
npx jest --randomize
dotnet test -- xunit.execution.DisableParallelization=falseRun 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 ↗- TypeScript
typescript/src/practices/parallel-test-execution
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Test Data ManagementWhere a test's data comes from decides whether the suite can run in parallel, twice in a row, or at all — builders, factories, fixtures and per-test isolation.
- Flaky TestsWhy tests fail intermittently, the six root causes and how to fix each one, how to detect flakiness deliberately, and what to do with a test you cannot fix today.
- Playwright ShardingSplitting a browser suite across machines and merging the reports back — the mechanics, the prerequisites, and how to choose a shard count that is actually faster.
- Integration TestingTesting your code against real dependencies — databases, HTTP clients, message brokers — with Testcontainers, and what belongs at this level rather than above or below it.
- TestcontainersRunning real databases, brokers and services as disposable containers from inside your test suite — the pattern that made integration testing cheap.
- Testing in JenkinsDeclarative pipelines, parallel stages, agent control and JUnit reporting — how to run a modern test suite on the CI server you probably inherited.
- Writing Tests in Pythonpytest fixtures, the patching rules that catch everyone, async testing, and the toolchain for a Python project that has to hold up in CI.
- Writing Testable CodeThe properties that make code easy to test — pure cores, injected edges, no hidden state — and the specific smells that make it hard.