A team doubled their CI machine size and set workers to 16. The suite got slower and started failing tests that had never failed. They concluded Playwright did not scale.
Playwright scaled fine. They had passed the optimum, and past the optimum every additional worker makes things worse in two ways at once.
Why more workers stops helping.
Each worker is a browser process. Browsers are memory-hungry. When workers exceed available memory, the machine swaps, and everything slows down at once rather than gracefully.
CPU contention slows every worker. With more workers than cores, the operating system time-slices between them. Each test now takes longer in wall-clock time, and tests have timeouts measured in wall-clock time. This is the mechanism that turns "slower" into "flaky": a test that took 4 seconds now takes 11 and trips a 10-second timeout. Nothing about the test changed.
The application becomes the bottleneck. Sixteen workers means sixteen concurrent users hammering one staging server. If the app slows under that load, every test slows, and you are now load-testing by accident.
Finding the optimum. Measure, do not reason:
for w in 2 4 6 8 12; do
echo "workers=$w"
time npx playwright test --workers=$w
done
The curve is almost always the same shape: strong improvement, then flattening, then reversal. Take the number just before flattening, not the peak, because the peak has no headroom for a noisy CI machine.
Typical answers are lower than people expect. Four to eight on a standard CI runner. The default (half your cores) is a reasonable starting point precisely because it leaves headroom.
Set it explicitly in CI:
workers: process.env.CI ? 4 : undefined,
Fixed on CI so runs are reproducible and comparable week to week. Automatic locally, where machines differ.
Do not forget the server side. If your staging environment is one small container, no amount of worker tuning helps. Sometimes the right fix is scaling the application under test, and noticing that is a senior observation most people miss because they only look at the test side.
fullyParallel and what it changes. With it on, tests within a file run in parallel too. More parallelism, and it exposes any file-level shared state immediately. Leave it on: the failures it surfaces were always real.
Where a file really must run in order, be surgical:
test.describe.configure({ mode: 'serial' });
Every serial block is a group that cannot be sped up and whose tests all fail together when the first one does. Treat each as a small debt with a reason recorded.
Timeouts and contention. Under load, generous timeouts look like the fix for the flakiness described above. They are not: they hide contention rather than resolving it, and they make genuine failures slow to report. Fix the contention. Reserve timeout increases for operations that really are slow, such as a report generation endpoint.
Remember this: measure the curve and take the point before it flattens. Flakiness under high worker counts is contention, not a race.
Reference: Parallelism.