- are flaky.
- take longer than necessary to execute.
- are hard-to-maintain.
bad-example.spec.ts
waitForTimeout
says:
Never wait for timeout in production. Tests that wait for time are inherently flaky. Use Locator actions and web assertions that wait automatically.But what about those situations where manual waiting seems like the only solution? I’ve created a somewhat contrived scenario where waiting seems to be the best solution, and will explain why it’s not while providing more maintainable alternatives in this guide.
When auto-waiting isn’t working, the page isn’t really ready
In the following contrived example, a UI that looks fully loaded and functioning starts reacting to user interactions only after five seconds. Buttons that should open a modal, open it after this artificially added delay.While this scenario might seem extreme, the pattern of unresponsive UI is very common. Modern Frontend Frameworks often deliver static and unresponsive HTML that starts functioning after loading the required JavaScript. This adding of functionality is called hydration, and websites often appear unresponsive and broken before all code is loaded.I know it seems odd that I’ve added a giant HTML file here with embedded JS, but we will want to refer to the page structure
modalLoader.html

failing-test.spec.ts
fixed-with-timeout.spec.ts
- Your users don’t know to wait this critical five seconds before the button becomes functional. The user experience is broken and you may well be hiding or disabling features to avoid user frustration and rage clicking.
- If this were a real-world scenario, it’s likely that third-party dependencies or other latency causes the modal to not be ready. If that’s the case, an arbitrary timeout isn’t going to work every time. It will be either too short (leads to test failure) or too long (leads to slow tests).

1. Edit the page: give a role only when the modal is ready
As the team writing synthetic monitoring, testing the final rendered page, we may not have the scope or even the access to edit the code underlying the page we’re monitoring. However it’s worth noting that some structural changes to the page would fix this test and improve usability. In this case, I recommend disabling the<button>
until the underlying modal is available.
modalLoader.html
locator.click()
will wait for the button to be enabled and only click it once it’s ready.
modalLoader.html
auto-waiting.spec.ts
2. Use a degraded state
This solution is fairly situational, but it’s worth considering if the modal you’re checking for isn’t completely necessary for your test to move forward. For example, if you’re checking modal details, but the next step is to close the modal and move elsewhere on the page, consider having the test enter a degraded state rather than failing. A full walkthrough of the code changes is on our documentation site, but suffice to say that with soft assertions you can have a check enter a ‘yellow’ state on the Checkly dashboard without triggering the same alerts as a failing check. This is perfect for performance issues that are intermittent, and would otherwise cause constant downtime alerts. To see a demo of how this state works in Checkly, take a look at Stefan’s tutorial video:3. Monitor network traffic with waitForResponse
Playwright has built-in functions to wait for an event before continuing with execution. Waiting for the network is a good example; you can wait for any request or response before continuing.
This example waits for any response matching the given pattern:
network-waiting.spec.ts
Note that
waitForResponse()
isn’t limited to URL matching, as it also accepts patterns.waitForResponse
lets us access and wait for response details, too.
advanced-network-waiting.spec.ts
A note on code reading: for myself, and many other users on Stack Overflow, it’s quite difficult to get the promise structure here right: there is no await in the
responsePromise
definition, rather await
is used only when we create a variable with the return from responsePromise
.4. Implement your own auto-waiting and retry mechanism
If there’s no way to change the application code and no obvious network request to wait for, you could also make your tests pass by implementing you own retry mechanisms. If we consider the modal example, you can try to repeat your actions until they pass. If your first button click doesn’t yield the expected results (showing the modal), try it again until it does. Playwright’stoPass
assertion allows you to implement precisely this functionality.
retry-with-topass.spec.ts
toPass
method, Playwright will retry these actions until they pass. If the modal isn’t opening after the first click, it will try again until it is able to close it again or timeout and fail your test entirely.
This approach won’t fix the application’s UI/UX issues but allows you to “work around” them in your end-to-end tests.
Stefan explains it in detail on YouTube if you want to learn more about this approach.
Conclusions
In summary, while it may be tempting to use hard waits in your Playwright tests, they often create more problems than they solve. They can make your tests flaky, slow, and harder to maintain. Instead, Playwright provides many tools to handle dynamic scenarios:- Rely on auto-waiting whenever possible.
- Modify the page structure to better reflect readiness if you have access.
- Use degraded states for slow responses, rather than failing the test.
- Monitor network traffic with
waitForResponse
to sync with backend events. - Implement specific auto-waiting mechanisms with
toPass
.