📖 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)

1. Test Timeout (Default: 30s) The absolute limit for the entire test function. If exceeded, test aborts immediately.
⬇️ Overrides everything below
2. Expect / Action Timeout (Default: 5s) Limits individual assertions (toBeVisible) and actions (click, fill).
⬇️ Can be overridden per-call
3. Per-Call Timeout (Custom) e.g., await page.click('#btn', { timeout: 15000 })

⏱️ Test Timeout

The total time allowed for a single test to run. This includes setup, execution, and teardown.

typescript · playwright.config.ts
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.

typescript
// 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.

typescript
// 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.

Controls how long page.goto() and page.waitForURL() will wait for the page to load.

typescript
// 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.

typescript
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.

typescript
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

typescript · ❌ Don't do this
// ❌ 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

typescript · ❌ Impossible state
// ❌ 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

  1. Keep global test timeouts at 30s. If a test takes longer, it's probably doing too much or waiting for something broken.
  2. Use test.slow() for inherently slow tests instead of calculating exact milliseconds.
  3. Use per-action { timeout: ... } for specific slow operations (uploads, exports).
  4. Never increase global timeouts to fix flakiness. Flakiness means the app is broken or the test is wrong. Higher timeouts just delay the failure.
  5. 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

Beginner
What is the default test timeout in Playwright?
Show Answer
The default test timeout is 30 seconds. The default assertion/action timeout is 5 seconds.
Intermediate
What happens if you set an action timeout of 60 seconds, but the test timeout is 30 seconds?
Show Answer
The test will fail at 30 seconds with a test timeout error. The test timeout is a hard ceiling for the entire test function. Action timeouts must be less than the remaining test time to execute fully.
Intermediate
What is the difference between 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.
Advanced
Why is increasing the global timeout to fix a flaky test considered an anti-pattern?
Show Answer
Flakiness usually indicates a race condition, a missing wait, or an environmental issue. Increasing the global timeout just delays the inevitable failure and wastes CI time for all other tests. The correct approach is to fix the auto-waiting logic (e.g., waiting for a specific element) or apply a local per-action timeout.

🏋️ Practical Exercise

🎯 Hands-On Practice Medium

Audit Your Timeouts

  1. Open your playwright.config.ts file.
  2. Check your timeout and actionTimeout values. Are they higher than the Playwright defaults (30s/5s)?
  3. If yes, find the specific tests that caused the inflation.
  4. Revert the global config back to defaults.
  5. For the specific slow tests, add test.slow() or test.setTimeout() locally.
  6. 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.

  1. Configure your global test timeout to 10 seconds (do not change this).
  2. Make the test pass without changing the global timeout.
  3. Hint: You will need to use test.slow() or test.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.

1. What is the default test timeout in Playwright?
A) 10 seconds
B) 30 seconds
C) 60 seconds
D) 5 seconds
Answer: B — The default test timeout is 30 seconds. The default expect/action timeout is 5 seconds.
2. What does test.slow() do?
A) Adds a 10-second sleep to the test
B) Disables auto-waiting
C) Multiplies the current test timeout by 3
D) Slows down the browser's network speed
Answer: C — test.slow() triples the test timeout, marking it as inherently slow without calculating exact milliseconds.
3. If your global test timeout is 30s, and you set a click action timeout to 45s, what happens if the click takes 40s?
A) The click succeeds at 40s
B) The test fails at 30s with a test timeout error
C) The test fails at 45s with an action timeout error
D) Playwright throws a configuration error immediately
Answer: B — The test timeout is the hard ceiling. The test will be aborted at 30 seconds before the 45-second action timeout can even be reached.
4. Which configuration property controls how long expect(locator).toBeVisible() will retry?
A) timeout
B) actionTimeout
C) expect.timeout
D) navigationTimeout
Answer: C — The expect object in the config has a timeout property that controls assertion polling (default 5000ms).
5. Why is increasing the global test timeout to fix flakiness an anti-pattern?
A) It makes the tests run slower in CI when they fail
B) It masks the real problem (missing waits or broken app)
C) It applies to all tests, even fast ones
D) All of the above
Answer: D — Inflating global timeouts wastes CI time, masks real bugs, and affects the entire suite. Use targeted local timeouts instead.
6. How do you apply a 15-second timeout to a single specific click() action?
A) page.click('#btn', { timeout: 15000 })
B) page.click('#btn', 15000)
C) test.setTimeout(15000); page.click('#btn')
D) page.click('#btn', { actionTimeout: 15000 })
Answer: A — Pass the timeout property inside the options object directly to the action method.
7. What is the relationship between auto-waiting and timeouts?
A) They are the same thing
B) Auto-waiting is the mechanism; timeout is the budget for that mechanism
C) Timeouts replace auto-waiting when set manually
D) Auto-waiting only works if timeouts are disabled
Answer: B — Auto-waiting polls the DOM continuously. The timeout defines the maximum duration it will keep polling before giving up.
8. What happens when a test timeout expires in Playwright?
A) The test hangs forever
B) Playwright kills the Node process immediately
C) Playwright gracefully aborts the operation, captures a trace, and cleans up the browser context
D) It automatically retries the test from the beginning
Answer: C — Playwright performs graceful cleanup, saves debugging artifacts (screenshots/traces), and closes the context before marking the test as failed.
9. Which of the following is the correct way to set a global action timeout in playwright.config.ts?
A) timeout: 10000
B) expect: { timeout: 10000 }
C) use: { actionTimeout: 10000 }
D) actions: { timeout: 10000 }
Answer: C — Global action timeouts are set inside the use object using the actionTimeout property.
10. When should you use test.setTimeout() vs test.slow()?
A) Use setTimeout for exact control; use slow for relative scaling
B) Use setTimeout for fast tests; use slow for slow tests
C) They are identical functions
D) Use slow for actions; use setTimeout for assertions
Answer: A — setTimeout(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() or test.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.