A Playwright test may pass several times and then fail without any obvious change to the application. A button that was clickable yesterday might time out today. A page may load correctly on a developer's computer but fail in CI. Sometimes, a test passes when run alone and fails when the entire test suite runs.
These are common signs of flaky tests.
Flakiness can come from timing assumptions, asynchronous operations, shared test data, network delays, animations, browser state, or differences between local and CI environments. Increasing timeouts may hide some symptoms, but it rarely explains the underlying problem.
Playwright Trace Viewer helps developers investigate these failures by recording what happened during a test. It can show actions, page snapshots, network requests, console messages, and other diagnostic information, depending on the trace configuration.
This article explains how to capture traces, inspect failures, and use the evidence to fix unreliable Playwright tests.
What Is Playwright Trace Viewer?
Trace Viewer is a debugging tool for examining Playwright test execution. A trace contains a timeline of test actions and supporting information about what happened before, during, and after those actions.
Depending on the recording settings, a trace can include:
- Test steps and their execution times.
- DOM snapshots before and after actions.
- Screenshots associated with recorded actions.
- Network requests and responses.
- Console messages and page errors.
- Locator details and actionability information.
- Source locations for test steps.
This information is useful when a failure cannot be reproduced consistently.
For example, suppose a test clicks a Save button and waits for a success message. The test fails in CI after waiting for several seconds. A trace can help determine whether the button was unavailable, the click never happened, the request failed, or the server responded successfully but the interface did not update.
Without a trace, developers may have only a timeout message and a screenshot of the final page. With a trace, they can inspect the sequence of events that led to the failure.
Configure Playwright to Record Traces
For Playwright Test, trace recording can be configured in playwright.config.ts.
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
use: {
baseURL: "http://127.0.0.1:3000",
trace: "retain-on-failure"
},
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"]
}
}
]
});The retain-on-failure setting records traces for tests and retains them when a test fails. This is a useful default for debugging flaky tests without keeping every successful trace.
Playwright supports several trace modes:
| Trace mode | Behavior |
|---|---|
off | Does not record traces |
on | Records traces for all tests |
retain-on-failure | Records traces and retains them for failed tests |
on-first-retry | Records a trace when a test is retried for the first time |
retain-on-first-failure | Records traces and retains them for the first failed attempt |
For CI pipelines, retain-on-failure or on-first-retry is often a reasonable starting point. Recording every test can generate a large amount of diagnostic data, especially in large suites.
Choose a mode based on the problem you are investigating. If a test fails on its first attempt, on-first-retry may still provide a trace when the retry runs, but that trace may represent the retry rather than the original failure. When investigating first-attempt failures, use a configuration that records the initial attempt as well.
Create a Test That Exposes the Problem
Consider a test that updates a user's profile and expects a confirmation message.
import { test, expect } from "@playwright/test";
test("user can update profile", async ({ page }) => {
await page.goto("/profile");
await page.getByLabel("Display name").fill("Aarav Kumar");
await page.getByRole("button", {
name: "Save"
}).click();
await expect(
page.getByText("Profile updated")
).toBeVisible();
});This test uses Playwright's locator APIs and an auto-retrying assertion. The assertion waits for the confirmation message to become visible within its timeout.
However, the test can still fail for several reasons:
- The profile page takes too long to load.
- The application rejects the update.
- The Save button is disabled while another operation runs.
- The server returns an error.
- The confirmation message differs from the expected text.
- The test uses a profile that another test has modified.
The trace helps distinguish these possibilities. Instead of immediately increasing the timeout, inspect what actually happened during the failed run.
Open a Trace in Trace Viewer
After a test fails, Playwright Test commonly attaches trace files to its test results or report, depending on the reporter and configuration.
You can inspect a local trace with the Playwright CLI:
npx playwright show-trace path/to/trace.zipReplace the example path with the actual trace file produced by your test run.
You can also use the Playwright HTML report to inspect the failed test and open its trace when the trace is available as an attachment.
npx playwright show-reportThe HTML report is particularly useful in CI because it organizes tests by project, file, and result. If your pipeline uploads the Playwright report as an artifact, developers can download it and investigate the failure without rerunning the test immediately.
For security-sensitive applications, remember that traces can contain page content, URLs, request details, and other diagnostic data. Store and share them according to your organization's data-handling policies.
Understand the Trace Viewer Timeline
Once the trace is open, start with the action timeline. It shows the sequence of operations performed by the test and helps you locate the first unexpected event.
A useful investigation usually follows this order:
- Locate the first failed action or assertion.
- Inspect the actions immediately before the failure.
- Examine the page snapshot at the relevant point.
- Check associated network requests and console errors.
- Compare the recorded behavior with the test's assumptions.
Do not focus only on the final failure message. A timeout may be the last symptom of an earlier problem.
For example, if the Save button click succeeds but the confirmation assertion fails, the click itself may not be the issue. The network request might have returned a validation error, or the application might have displayed a different message.
Inspecting the steps before the assertion helps narrow the investigation.
Use Snapshots to Understand Page State
Trace Viewer provides snapshots of the page at different points in the test. These snapshots help answer a basic question: what did the page look like when the test tried to interact with it?
Suppose a locator times out while searching for a button named Save. The snapshot may reveal that:
- The page was still displaying a loading indicator.
- A modal dialog was covering the form.
- The expected button had a different accessible name.
- A validation message prevented the form from being submitted.
- The page had navigated to an unexpected route.
The snapshot can expose an incorrect assumption in the test without requiring additional logging.
If the application renders asynchronously, inspect the state immediately before the failed action rather than assuming that the page reached its expected state after navigation.
For example, this test may be unreliable if the application renders the form after an asynchronous data request:
await page.goto("/profile");
await page.getByLabel("Display name").fill("Aarav Kumar");A better test waits for the actual field to become available through its locator:
await page.goto("/profile");
const displayName = page.getByLabel("Display name");
await expect(displayName).toBeVisible();
await displayName.fill("Aarav Kumar");Playwright actions perform their own actionability checks, so an explicit visibility assertion is not required before every action. Add assertions when they communicate an important expected state or help diagnose the behavior under test.
Investigate Network Requests
Network activity is often the key to understanding a flaky test.
Suppose a profile update fails because the server occasionally returns HTTP 500. Increasing the timeout will not fix the server error. A trace can help you inspect the request, response status, timing, and related browser activity.
Look for patterns such as:
- A request that never completes.
- An unexpected 401 or 403 response.
- A 404 caused by an incorrect route.
- A 429 response caused by rate limiting.
- A 500 response from the application or a dependent service.
- Multiple requests when the test expected only one.
If the request fails consistently, investigate the application or test configuration. If it fails intermittently, compare successful and failed traces to identify differences in response timing, request parameters, authentication, or test data.
You can also assert the expected response directly when it is part of the behavior being tested.
import { test, expect } from "@playwright/test";
test("profile update succeeds", async ({ page }) => {
await page.goto("/profile");
await page.getByLabel("Display name").fill("Aarav Kumar");
const responsePromise = page.waitForResponse(
response =>
response.url().includes("/api/profile") &&
response.request().method() === "PUT"
);
await page.getByRole("button", {
name: "Save"
}).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
await expect(
page.getByText("Profile updated")
).toBeVisible();
});This example assumes that the application sends a PUT request to an endpoint containing /api/profile and returns HTTP 200 on success. Adjust the method, URL, and expected status to match the actual API contract.
Registering the response wait before clicking is important. If the test starts waiting only after the click, a fast response might arrive before the wait is registered.
Also, avoid checking only the HTTP status when the UI behavior matters. The application could receive a successful response and still fail to update the interface correctly.
Fix Race Conditions Instead of Adding Fixed Delays
One common cause of flaky tests is using fixed delays to guess when an operation will finish.
For example:
await page.goto("/dashboard");
await page.waitForTimeout(3000);
await page.getByRole("button", {
name: "Refresh"
}).click();This test waits three seconds regardless of whether the dashboard is ready. On a fast machine, the delay wastes time. On a slow CI runner, three seconds may not be enough.
Use an observable condition instead:
await page.goto("/dashboard");
await expect(
page.getByRole("heading", {
name: "Dashboard"
})
).toBeVisible();
await page.getByRole("button", {
name: "Refresh"
}).click();If the dashboard depends on data loading, wait for the relevant data or UI state rather than an arbitrary duration.
For example, if a table displays the number of active jobs, assert that the expected row or status appears before interacting with it.
Fixed waits can still be useful for narrowly defined timing tests, but they should not be the default synchronization mechanism for application behavior.
Check Whether Retries Are Hiding a Real Problem
Playwright supports test retries, which can help collect more information about intermittent failures. Retries are not a substitute for fixing the cause.
You can configure retries in the test configuration:
import { defineConfig } from "@playwright/test";
export default defineConfig({
retries: process.env.CI ? 2 : 0,
use: {
trace: "on-first-retry"
}
});In this configuration, CI retries failed tests up to two times, and traces are recorded on the first retry.
A test that fails initially but passes on a retry may indicate a race condition, shared state, network instability, or another timing-sensitive dependency. It should be investigated rather than automatically considered reliable.
Pay attention to the difference between a test that fails every time and a test that fails only on the first attempt. The former may have a deterministic bug or incorrect assertion. The latter often suggests that execution order, timing, or environment state matters.
Retries are useful for collecting evidence, but a suite with many retry-dependent tests can create false confidence and slow down delivery.
Investigate Shared Test Data and Browser State
Not every flaky test is caused by timing. Two tests may modify the same database record, use the same account, or depend on a shared resource.
For example, one test may rename a profile while another test expects its original name. Both tests might pass individually but fail when executed concurrently.
To investigate this, compare traces from isolated and full-suite runs. Check which account or record was used, whether the expected initial state existed, and whether another operation modified the data.
Use unique test data where practical. Reset application state before each test, or create dedicated records for each test run. When a test depends on shared infrastructure, make the dependency explicit and control access to it.
Browser state can also cause problems. Cookies, local storage, and authentication sessions may differ between tests. Playwright's isolated browser contexts help prevent many forms of state leakage, but applications and external services can still share data across contexts.
A trace can help identify an unexpected authenticated user or a page state that differs from the test's assumptions.
Record Additional Diagnostic Information When Needed
Trace Viewer is often enough to identify the cause, but some failures require additional evidence.
For example, you may need to inspect a browser console error or verify which environment the test accessed. Playwright can capture console messages during a run:
import { test } from "@playwright/test";
test("dashboard loads", async ({ page }) => {
page.on("console", message => {
if (message.type() === "error") {
console.error(
"Browser console error:",
message.text()
);
}
});
page.on("pageerror", error => {
console.error(
"Uncaught page error:",
error.message
);
});
await page.goto("/dashboard");
});This logging can supplement a trace when the browser console is relevant to the failure. Keep the output focused so that important messages are not buried in noise.
Do not log passwords, authentication tokens, payment information, or other secrets. Diagnostic output is frequently retained in CI logs and shared with developers who may not have access to production data.
For a reproducible investigation, capture the browser version, operating system, test project, application build, and relevant configuration alongside the failure report.
A Practical Workflow for Debugging Flaky Tests
When a test fails intermittently, follow a consistent process:
- Enable trace recording for failed tests or retries.
- Reproduce the failure locally or in CI when possible.
- Open the trace and identify the first unexpected event.
- Inspect snapshots and actions around the failure.
- Check network requests and browser errors.
- Compare successful and failed runs.
- Determine whether the cause is synchronization, application behavior, shared state, or environment configuration.
- Fix the underlying issue and run the test repeatedly.
- Run the surrounding test suite to check for order-dependent failures.
- Keep retries as a diagnostic or resilience measure, not as the only solution.
This approach is more effective than making several unrelated changes at once. A focused fix also makes it easier to confirm that the original failure has been addressed.
Conclusion
Playwright Trace Viewer gives developers a detailed record of test execution, making it easier to understand failures that are difficult to reproduce. Its timeline, snapshots, network information, and action details help distinguish synchronization problems from application bugs and test-data issues.
Configure trace recording for failed tests, inspect the steps leading to the failure, and use the evidence to correct the underlying cause. Avoid relying on fixed delays or retries to conceal unreliable behavior.
A dependable browser test suite comes from clear assertions, controlled test data, observable synchronization points, and consistent diagnostics. Trace Viewer helps turn intermittent failures into problems that can be investigated and fixed systematically.

Join the conversation! Your thoughts help the community grow.