Parallelism in E2E is not only about speed but about honesty. When tests run one by one, hidden dependencies between them go unnoticed: a shared user, a shared order, a shared counter calmly survive a sequential run. Run the same tests simultaneously - and these dependencies surface as failures. So parallelism works as a detector: it does not create shared-state problems but exposes the ones that were already there.
Playwright has two different scaling mechanisms. Workers are independent processes on one machine: the --workers=4 flag runs four tests at once, each in its own process with isolated state. Shards split the whole suite across different CI machines: --shard=1/3 runs the first third of the tests, and three parallel CI jobs together pass the whole suite three times faster. The first scales within a machine, the second across machines.
Both mechanisms rely on fullyParallel and require data discipline. So that four workers or three shards do not interfere with each other, data must have a namespace - each test works with its own entities marked by its identifier. It is exactly the uniqueness from the data chapter - an email with workerIndex, separate accounts - that makes parallelism safe. Without it a parallel run turns into a race for shared records.
It helps to gather the scaling commands together once, to distinguish their purpose. Below is a local run in four workers and splitting the suite into three shards for CI. You return to this map when tuning run performance: workers increase the load of one machine, sharding spreads the suite across several, and you pick them for what you have - a powerful machine or several runners.
The conditions for safe parallelism go beyond data. Accounts must not conflict - otherwise two tests signed in as the same user and changing it will collide. The external environment must withstand the load - the test database, the API, third-party sandboxes get many times more requests at once. If even one of these conditions is unmet, parallelism will not speed up the suite but make it unstable and start failing it on resource limits.
The key principle is to scale after measuring, not by intuition. It seems more workers are always faster, but that is wrong beyond the environment's resources. If four workers already push the test database to its connection ceiling, eight will not speed up the run but slow it down and add flakiness on timeouts and locks. The optimum is where parallelism and the environment's capacity are balanced, and you find it by measurement, not by doubling at random.
You measure concrete quantities, not a general feeling. The run duration at different worker counts, CPU load, the size of the database connection pool, the rate limits of external services. These numbers show where the bottleneck is: if adding workers stops reducing the time, the matter is not parallelism but the capacity of the database or the external environment, and adding workers further is pointless - you must widen the bottleneck or stop.
The typical parallelism failures are predictable. Shared mutable data and accounts surfacing as failures exactly under a parallel run - tests green one by one and red together. Adding workers without measuring, pushing the test database to its limit and adding flakiness instead of speed. And ignoring the external environment's limits under parallel load. Give tests namespaced data, separate accounts and scale by measurement, not by faith in "the more, the faster".
# Locally: four independent processes on one machine
npx playwright test --workers=4
# CI: split the suite into three shards across different machines
npx playwright test --shard=1/3
npx playwright test --shard=2/3
npx playwright test --shard=3/3