Parallelism, Retries, and Timeouts
How Playwright runs many tests at once safely, when a retry genuinely helps versus when it just hides a real bug, and the layered timeout settings that actually control a test's patience.
What you'll learn
- Explain what Playwright's worker-based parallelism model isolates and what it doesn't
- Distinguish a legitimate use of test retries from retries masking a real bug
- Identify which of Playwright's several timeout settings actually governs a given failure
Prerequisites
Explanation
Playwright runs tests in parallel using multiple workers — separate OS processes, each running test files one at a time within itself, with several workers running concurrently. This is what makes Playwright's context-per-test isolation (from this course's first lesson) load-bearing at scale: since each test gets a fresh context regardless of which worker or file it runs in, tests genuinely don't interfere with each other's browser state even when running simultaneously. What parallelism does not isolate on its own is anything external to the browser — a shared database, a shared file the test writes to, a fixed port a local server binds to — which is exactly why the test-data isolation from the previous lesson (unique data per test) remains necessary even with Playwright's own context isolation already in place.
Retries (retries: 2 in config, or --retries=2 on the command line) re-run a failed test up to the configured number of additional times before reporting it as truly failed. This is a genuine, useful tool for a specific, narrow purpose: absorbing rare, environmental flakiness (a CI runner under momentary load, a truly transient network blip) that isn't a bug in the application or the test. It is not a substitute for fixing a real, reproducible bug or a genuinely flaky test — a test that only passes 7 times out of 10 because of a real race condition in the application will often still fail intermittently even with retries, just less visibly and less often, which can let a real bug quietly ship while the suite reports green more often than it should. Retries should reduce noise from genuine environmental randomness, not paper over an actual defect.
Playwright layers several distinct timeouts, and diagnosing a timeout failure correctly means knowing which one actually applies: the test timeout (default 30s) bounds an entire test's total runtime; the expect timeout (default 5s) bounds how long a single web-first assertion polls before giving up; action timeouts bound how long a single action (like .click()) waits for actionability; and the global timeout bounds the entire test run. A test failing with "Timeout 5000ms exceeded" while the test itself has 20 more seconds of budget left is very likely an assertion timeout, not a test timeout — misreading which layer actually fired, and blindly increasing the wrong one (or all of them, "just in case"), is a common mistake that hides the real diagnostic signal a specific timeout's failure was actually giving you.
Example
Modeling worker-based parallelism's isolation boundary, and the layered-timeout diagnostic reasoning, as data.
function isIsolatedByPlaywrightContext(resourceType) {
// Playwright's per-test context isolates browser-side state; it does NOT
// isolate anything external to the browser on its own.
const browserIsolated = ["cookies", "localStorage", "sessionStorage", "page-dom"];
return browserIsolated.includes(resourceType);
}
console.log(isIsolatedByPlaywrightContext("cookies")); // true
console.log(isIsolatedByPlaywrightContext("shared-database")); // false -- needs its own isolation strategy (unique test data)
function diagnoseTimeout(errorMessage, testElapsedMs, testTimeoutMs) {
if (errorMessage.includes("Timeout") && errorMessage.includes("exceeded") && testElapsedMs < testTimeoutMs) {
return "likely an assertion or action timeout, not the overall test timeout";
}
if (testElapsedMs >= testTimeoutMs) {
return "the overall test timeout was reached";
}
return "not a timeout-related failure";
}
console.log(diagnoseTimeout("Timeout 5000ms exceeded", 8000, 30000)); // assertion/action timeout, not test timeoutTry it yourself
Call diagnoseTimeout with testElapsedMs equal to testTimeoutMs, and confirm it correctly reports the overall test timeout instead.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
Write needsIndependentIsolationStrategy(resourceType) modeling which resources Playwright's per-test context does NOT isolate on its own -- return true for 'shared-database', 'shared-file', or 'fixed-port'; false for 'cookies', 'localStorage', or 'page-dom' (which Playwright's context isolation already handles).
Checks: identifies a shared database as needing independent isolation · correctly recognizes cookies as already isolated · identifies a fixed port as needing independent isolation
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
Write shouldRelyOnRetries(failureReason) returning true ONLY for 'transient-ci-load' or 'momentary-network-blip' (genuine environmental flakiness) -- false for 'race-condition-in-app', 'flaky-locator-matches-multiple', or any other reason (these are real bugs retries would only mask, not fix).
Checks: correctly identifies genuine environmental flakiness · correctly rejects masking a real application bug with retries · correctly rejects masking a real test-authoring bug with retries
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Common mistakes
- Assuming Playwright's per-test context isolation covers everything a test touches -- it only isolates browser-side state (cookies, storage, DOM); a shared database, file, or port needs its own, separate isolation strategy.
- Adding retries to make a genuinely flaky (buggy) test 'pass reliably' instead of diagnosing and fixing the real race condition or ambiguous locator causing it -- this hides the bug rather than fixing it, and it can still fail intermittently, just less visibly.
- Increasing every timeout setting 'just in case' when one specific timeout (often the 5-second expect timeout) is actually the one firing -- this treats the symptom without understanding which layer's failure was the real diagnostic signal.
Knowledge check
Takeaway
Playwright's context-per-test isolation only covers browser-side state, not external resources like a shared database — those still need unique test data; retries exist to absorb genuine environmental flakiness, not to mask a real bug; and diagnosing a timeout correctly means identifying which of several layered timeout settings actually fired.
Summary
Workers run tests in parallel with context-per-test browser isolation, but external resources (databases, files, ports) need their own isolation strategy. Retries should absorb genuine environmental flakiness only, never mask a real, reproducible bug. Playwright has distinct test, expect, action, and global timeouts — diagnosing a failure means identifying which one actually fired.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.