Sitemap

If Playwright Has Auto-Waiting, Why Do We Still Need Explicit Waits?

4 min readJun 15, 2026

--

Press enter or click to view image in full size
Playwright Auto-Waiting vs Explicit Waits — Explained Visually

One of the most common questions from engineers transitioning to Playwright is:

“If Playwright already has auto-waiting, why do we still need explicit waits?”

At first glance, it seems like auto-waiting should eliminate synchronization issues entirely. After all, Playwright is designed to automatically wait for elements before interacting with them.

However, while auto-waiting significantly reduces the need for manual waits, it does not completely replace them. Understanding the difference is essential for writing stable, reliable, and maintainable tests.

Understanding Playwright Auto-Waiting

Auto-waiting (Recommended)

Playwright automatically waits for elements to become actionable before performing actions like:

  • click()
  • fill()
  • check()
  • hover()
  • selectOption()

When you perform an action such as:

await page.locator('#submit').click();

Playwright automatically waits until the element is:

  • Present in the DOM
  • Visible
  • Stable (not moving or animating)
  • Enabled
  • Able to receive user interactions

Similarly, assertions automatically retry until the expected condition becomes true or a timeout occurs:

await expect(locator).toHaveText('Success');

This means many traditional Selenium-style waits are no longer necessary.

For example, this pattern is usually redundant:

await page.waitForSelector('#submit');
await page.click('#submit');

Playwright already performs the required waiting before the click.

This is one of the reasons Playwright tests are often cleaner and less flaky than tests written with older automation frameworks.

Advantages

  • Less code
  • More reliable
  • Fewer flaky tests
  • Faster when elements are already ready

The Limitation of Auto-Waiting

Auto-waiting only understands element readiness.

It can determine:

  • Is the element visible?
  • Is it enabled?
  • Can it be clicked?

What it cannot determine is whether your application is actually ready from a business perspective.

This distinction is critical.

Consider:

await page.locator('#save').click();

Playwright can ensure the button is clickable.

But it cannot answer questions such as:

  • Has the backend finished processing?
  • Has all data loaded from the API?
  • Has a background job completed?
  • Has the application state been initialized?

These are application-specific conditions that Playwright cannot automatically determine.

That’s where explicit waits become useful.

When Explicit Waits Are Still Necessary

1. Waiting for Business Logic

A common scenario is when a button becomes visible before the underlying data has loaded.

For example:

await page.waitForResponse(
response =>
response.url().includes('/api/users') &&
response.status() === 200
);

Here we’re waiting for an API call to complete before continuing.

Auto-waiting cannot know that your next action depends on this particular network response.

2. Waiting for Loading Indicators to Disappear

Many applications display spinners or loading overlays while processing data.

In these cases, you may need to wait until the loading state disappears:

await page.waitForSelector('.spinner', { state: 'hidden' });

A better Playwright-style approach is:

await expect(page.locator('.spinner')).toBeHidden();

This ensures the application has completed loading before moving to the next step.

3. Waiting for Navigation or URL Changes

After actions such as login, Playwright may need explicit guidance about the expected destination.

Example:

await page.waitForURL('**/dashboard');

This verifies that navigation has successfully completed before continuing with the test.

Without this check, subsequent actions may execute before the application reaches the intended page.

4. Waiting for Network Activity

Modern Single Page Applications (SPAs) often make multiple asynchronous API calls after a page loads.

In some situations, you may need:

await page.waitForLoadState('networkidle');

This waits until network activity has settled.

However, use this carefully.

Many applications maintain long-running connections, polling requests, or analytics calls, making networkidle unreliable in some environments.

Whenever possible, wait for specific UI states or API responses instead.

5. Waiting for Custom Application Conditions

Sometimes readiness is defined by internal application state.

Example:

await page.waitForFunction(() =>
window.appState?.loaded === true
);

This allows the test to wait for a condition that only your application understands.

Again, this is something Playwright cannot automatically determine.

Example based on a practical scenario:

A common example is a product listing page, where several components and data requests load in parallel. To handle this reliably, we can use networkidle to wait until all network requests have completed and the page is fully rendered.

await this.menuOption.click();
// Verify that the user is redirected to the Products page
await expect(this.page).toHaveURL(
"https://www.passthenote.com/app/products"
);
// Wait until all network requests have finished
await this.page.waitForLoadState('networkidle');
Press enter or click to view image in full size

What You Should Avoid

One of the biggest anti-patterns in test automation is using arbitrary sleeps:

await page.waitForTimeout(5000);

While it may appear to solve timing issues, it usually creates flaky and inefficient tests.

Why Fixed Delays Are Bad

  • Five seconds may not be enough on a slow environment.
  • Five seconds may be unnecessarily long on a fast environment.
  • Test execution becomes slower.
  • Flakiness increases over time.

A test should wait for a condition, not for an arbitrary amount of time.

The Practical Rule of Thumb

Use the following approach when writing Playwright tests:

Use Auto-Waiting For

  • click()
  • fill()
  • check()
  • selectOption()
  • Most element interactions

Use Assertions For

  • Visibility checks
  • Text validation
  • State verification

Examples:

await expect(locator).toBeVisible();
await expect(locator).toHaveText('Success');

Use Explicit Waits Only For

  • Network responses
  • Navigation or URL changes
  • Loading indicators/ spinners disappearing
  • Dynamic content appearing/disappearing
  • Application-specific readiness conditions/ Custom application events

Examples:

await page.waitForResponse(...);
await page.waitForURL(...);
await expect(spinner).toBeHidden();
await page.waitForFunction(...);

Final Thoughts

Auto-waiting is one of Playwright’s most powerful features because it eliminates a large percentage of synchronization problems automatically.

However, auto-waiting only understands element readiness, not application readiness.

Think of it this way:

  • Auto-waiting = Playwright automatically figures out when an element is ready for interaction.
  • Explicit waits = You tell Playwright what condition means your application is ready.

In a well-designed Playwright test suite, most synchronization is handled through locators and assertions, while explicit waits are reserved for situations where the framework cannot understand the application’s internal state.

The goal isn’t to eliminate explicit waits completely — it’s to use them only when they communicate meaningful application behaviour.

--

--

Arpita Biswas
Arpita Biswas

Written by Arpita Biswas

QA Automation Architect | 15+ Yrs | ISTQB | Certified Ethical Hacker | Helping Engineering Teams Build Scalable Secure & AI-Driven Quality Engineering Solutions