Timeouts Configuration — Mastering the Clock
Timeouts are the safety net of your test suite. Set them too low, and slow networks cause flaky tests. Set them too high, and a broken app wastes hours of CI time. Learn to configure them with surgical precision.
📖 The Story: The Timeout Tax
David was an SDET lead at a logistics company. His team wrote 500 Playwright tests. Most tests ran in 2 seconds. But one test—uploading a massive 50MB PDF to the server—took 45 seconds.
Because the default Playwright test timeout is 30 seconds, the upload test kept failing. David's quick fix was to increase the global test timeout in playwright.config.ts to 60 seconds. The upload test passed.
Six months later, the development team broke the login page. Instead of logging in instantly, the login API hung forever. When CI ran that night, every single test tried to log in, waited 60 seconds, and then failed. A suite that usually took 10 minutes took 8 hours. The team arrived at work Monday morning to a broken CI pipeline and zero useful feedback.
David had fallen victim to the "Timeout Tax." He traded global safety for local convenience. The fix wasn't to increase the global timeout; it was to use a targeted timeout for the slow upload test.
Timeouts should be as local as possible. A global timeout is a blunt instrument; a per-action timeout is a scalpel.
🎯 Why Timeouts Matter
Safety Net
Prevents tests from hanging forever if the app freezes or a network request never resolves.
CI Speed
Fail fast. A broken feature shouldn't consume 60 seconds of CI time per test before failing.
Precision
Different operations need different limits. File uploads need 60s; button clicks need 5s.
🌍 Real World Example
In a healthcare portal, viewing a patient's historical lab results requires fetching 5 years of data from a legacy database. This API takes 10-15 seconds to respond. Meanwhile, searching for an appointment takes 0.5 seconds.
If you set a global action timeout of 20 seconds to accommodate the lab results, a broken appointment search will waste 20 seconds per test before failing. If you set it to 2 seconds, the lab results test will always fail.
The solution is to keep the global timeout at 5 seconds, and apply a specific 20-second timeout to the API call or button click that triggers the lab results fetch.
🧒 Explain Like I'm 10
Imagine a teacher giving a test. The teacher has a rule: "You have 30 minutes to finish."
But one question on the test is very hard and takes 45 minutes to solve. Instead of changing the rule for the whole class to 60 minutes (which means everyone else wastes 30 minutes staring at the wall if they get stuck), the teacher says: "For this one specific question, you have 45 minutes."
In Playwright, the 30 minutes is your test timeout. The extra time for the hard question is a per-action timeout.
🎓 Professional Explanation
Playwright operates on a hierarchy of timeouts. At the top is the test timeout, which limits the entire test function. Below it are expect timeouts, action timeouts, and navigation timeouts. If any sub-timeout exceeds the test timeout, the test will be killed by the test timeout first.
Playwright's architecture uses a single timer for the test itself. When you call page.click(), Playwright spawns a child timer for that action. If the child timer expires, it throws a TimeoutError. If the parent test timer expires, it aborts the test entirely, cleaning up all pending operations.
📊 The Timeout Hierarchy
Playwright Timeout Hierarchy (Top-Down)
await page.click('#btn', { timeout: 15000 })
⏱️ Test Timeout
The total time allowed for a single test to run. This includes setup, execution, and teardown.
import { defineConfig } from '@playwright/test'; export default defineConfig({ timeout: 30000, // 30 seconds per test (default) expect: { timeout: 5000 // 5 seconds per assertion (default) } });
👁️ Expect Timeout
Controls how long expect(locator).toBeVisible() will retry before failing.
// Global config (playwright.config.ts) expect: { timeout: 10000 } // 10 seconds for all assertions // Per-assertion override await expect(page.getByTestId('dashboard')).toBeVisible({ timeout: 15000 // 15 seconds for this specific assertion });
👆 Action Timeout
Controls how long actions like click, fill, and press will wait for actionability before failing.
// Global config (playwright.config.ts) use: { actionTimeout: 10000 // 10 seconds for all actions } // Per-action override await page.getByRole('button', { name: 'Upload' }).click({ timeout: 45000 // 45 seconds for this specific click });
Best Practice: Keep global action timeouts low (5-10s). Use per-action overrides for known slow operations like file uploads or complex data exports.
🌐 Navigation Timeout
Controls how long page.goto() and page.waitForURL() will wait for the page to load.
// Global config use: { navigationTimeout: 15000 } // Per-navigation override await page.goto('https://slow-app.com', { timeout: 60000, waitUntil: 'domcontentloaded' });
⚙️ Modifying Timeouts at Runtime
Sometimes you need to adjust timeouts dynamically based on test context.
test('complex data export', async ({ page }) => { // Increase timeout for this specific test only test.setTimeout(120000); // 2 minutes await page.goto('/export'); await page.getByRole('button', { name: 'Generate Report' }).click(); // Assertion uses global expect timeout (5s), which is fine here await expect(page.getByText('Export complete')).toBeVisible(); });
🐢 The Magic of test.slow()
Instead of calculating exact milliseconds, you can mark a test as "slow." Playwright will automatically multiply the test timeout by 3.
test('huge file upload', async ({ page }) => { test.slow(); // If default is 30s, this test now gets 90s await page.goto('/upload'); // ... upload logic });
Use test.slow() for tests that are inherently slow due to file I/O, large database seeds, or third-party API integrations. It clearly signals intent: "This test is slow, and that's expected."
⚠️ Common Mistakes
Mistake 1: Global Timeout Inflation
// ❌ BAD: Inflating global timeout because of one slow test export default defineConfig({ timeout: 120000 // 2 minutes for ALL tests }); // ✅ GOOD: Keep global low, override locally export default defineConfig({ timeout: 30000 // 30 seconds default }); test('slow test', async ({ page }) => { test.setTimeout(120000); // 2 minutes for THIS test });
Mistake 2: Setting Action Timeout > Test Timeout
// ❌ BAD: Action timeout is longer than test timeout export default defineConfig({ timeout: 30000, // Test kills at 30s use: { actionTimeout: 60000 // Action waits for 60s (will never reach this) } });
The test timeout is a hard limit. If your test timeout is 30s and an action timeout is 60s, the test will fail at 30s with a generic test timeout error, masking the real issue.
✅ Best Practices
- Keep global test timeouts at 30s. If a test takes longer, it's probably doing too much or waiting for something broken.
- Use
test.slow()for inherently slow tests instead of calculating exact milliseconds. - Use per-action
{ timeout: ... }for specific slow operations (uploads, exports). - Never increase global timeouts to fix flakiness. Flakiness means the app is broken or the test is wrong. Higher timeouts just delay the failure.
- Fail fast. A 5-second timeout on a broken login button saves 25 seconds of CI time per test.
🏗️ Senior Engineer Deep Dive
How Playwright Tracks Time
Playwright doesn't use standard setTimeout for test boundaries. It uses a precise monotonic clock. When a test starts, a deadline is calculated. Every async operation (action, assertion) checks the remaining time against this deadline. If an action's internal timeout would exceed the test deadline, Playwright can preemptively fail the action.
The Cleanup Mechanism
When a test times out, Playwright doesn't just kill the Node process. It gracefully aborts the pending browser operation, saves the trace file, captures a screenshot, closes the browser context, and reports the failure. This graceful cleanup is why timeout errors in Playwright provide such rich debugging context compared to Mocha or Jest.
Timeouts vs. Auto-Waiting
Engineers often confuse timeouts with auto-waiting. Auto-waiting is the mechanism that polls the DOM. The timeout is the budget for that polling. If auto-waiting checks an element every 100ms, and the timeout is 5000ms, it will check up to 50 times. If the element appears at 2.3s, auto-waiting succeeds and the test moves on. The timeout is only the worst-case scenario.
💼 Interview Questions
Show Answer
Show Answer
test.setTimeout(60000) and test.slow()?Show Answer
test.setTimeout(60000) sets the exact test timeout to 60 seconds. test.slow() multiplies the current test timeout by 3. If the default is 30s, test.slow() makes it 90s. slow() is better for readability when you just want to indicate a test needs more time.Show Answer
🏋️ Practical Exercise
Audit Your Timeouts
- Open your
playwright.config.tsfile. - Check your
timeoutandactionTimeoutvalues. Are they higher than the Playwright defaults (30s/5s)? - If yes, find the specific tests that caused the inflation.
- Revert the global config back to defaults.
- For the specific slow tests, add
test.slow()ortest.setTimeout()locally. - Run the suite. Verify the slow tests still pass, and the fast tests fail faster when broken.
🚀 Mini Project
🏗️ The Slow API Challenge
Write a test that navigates to a page with a button that triggers an API call taking 12 seconds to respond.
- Configure your global test timeout to 10 seconds (do not change this).
- Make the test pass without changing the global timeout.
- Hint: You will need to use
test.slow()ortest.setTimeout()locally, and potentially a per-action timeout on the button click or a web-first assertion waiting for the result.
📝 Quiz
Test your understanding. Click an option to check your answer.
test.slow() do?test.slow() triples the test timeout, marking it as inherently slow without calculating exact milliseconds.expect(locator).toBeVisible() will retry?timeoutactionTimeoutexpect.timeoutnavigationTimeoutexpect object in the config has a timeout property that controls assertion polling (default 5000ms).click() action?page.click('#btn', { timeout: 15000 })page.click('#btn', 15000)test.setTimeout(15000); page.click('#btn')page.click('#btn', { actionTimeout: 15000 })timeout property inside the options object directly to the action method.playwright.config.ts?timeout: 10000expect: { timeout: 10000 }use: { actionTimeout: 10000 }actions: { timeout: 10000 }use object using the actionTimeout property.test.setTimeout() vs test.slow()?setTimeout for exact control; use slow for relative scalingsetTimeout for fast tests; use slow for slow testsslow for actions; use setTimeout for assertionssetTimeout(ms) gives you exact millisecond control. slow() multiplies the current timeout by 3, which is useful for readability when exact timing isn't critical.❓ FAQ
Q: Can I disable timeouts entirely?
A: You can set timeout: 0 to disable timeouts. This is highly discouraged for automated tests as it can cause CI jobs to hang indefinitely. Only use it for local debugging.
Q: Does test.slow() affect expect and action timeouts?
A: No. test.slow() only multiplies the overall test timeout by 3. It does not change the 5-second default for individual assertions or actions.
Q: How do global hooks like beforeAll handle timeouts?
A: beforeAll and afterAll have their own separate timeout settings (default 60s in defineConfig via globalTimeout or specific hook timeouts).
📦 Summary
🎯 Key Takeaways
- Timeouts have a hierarchy: Test Timeout > Expect/Action Timeout > Per-call Timeout.
- Keep global defaults low (30s test, 5s action) to fail fast in CI.
- Use
test.slow()ortest.setTimeout()for specific slow tests instead of inflating global config. - Use per-action
{ timeout: ... }for inherently slow UI operations like file uploads. - Never set an action timeout longer than the test timeout.
- Timeouts are the budget for auto-waiting, not a replacement for it.
