The CI job is red. The log names the test, says a locator timed out, and stops there. You run the test on your laptop and it passes. You rerun the job and it passes there too, which is worse: now you have a failure you can't reproduce and a green build that explains nothing.
TL;DR
To debug a Playwright test that failed in CI with the trace viewer, first keep the trace of the attempt that failed, then read it in a fixed order.
- With retries on, set
trace: 'retain-on-failure-and-retries'. With retries off, use'retain-on-failure'. The usual'on-first-retry'keeps only the first retry's trace, never the failed first run. - Find the failed action in the Errors tab, then read its log and snapshots to see which check never passed.
- Filter the Network tab to the action before the failure to see whether the backend answered.
- For a flaky test, compare the failed attempt's trace with the passing retry's and find the step that differs.
The trace of the failed run would explain it. It holds every action, the page at each step and the network calls behind it. But the CI setup the Playwright docs recommend, trace: 'on-first-retry', keeps the retry's trace, not the first run's. When a flaky test fails and then passes on retry, the only trace you get is of the run that worked.
The fix starts with a trace mode that keeps the failing attempt: 'retain-on-failure', for one, keeps a failed run's trace even when a later retry passes. We'd pair it with a reading order that gets you from the red job to the root cause, whether that turns out to be a locator, a backend call or a race.
What a trace records, and which run it belongs to
Read the wrong trace and you end up debugging a run that never failed. When retries are enabled, a failing test is retried until it passes or until the maximum number of retries is reached. The trace mode decides which of those runs are recorded and which recordings are kept.
Trace Viewer is a GUI for exploring a recorded trace after the script has run, and the Playwright docs present traces as a way to debug tests that fail on CI. You don't watch the test live. You step through what it already did, as many times as you need.
The snapshots setting captures a DOM snapshot on every action and records network activity, so the page at the moment of a click and the API call behind it end up in the same file. The screenshots setting captures screenshots during tracing to build the timeline preview, and it defaults to true.
Playwright calls a test's initial run the "first run" and every run caused by retries a "retry". Every trace mode is defined in those terms: which runs get recorded, and which recordings are kept. 'on' records and keeps a trace for every run. 'retain-on-failure' records every run but keeps the trace only for runs that failed, even when a later retry passes. 'on-first-retry' records and keeps a trace only for the first retry. 'off' records no trace.
If your config never sets the mode, Playwright uses the default. The trace option sets whether to record a trace for each test, and it defaults to 'off'.
Set tracing through the trace option rather than starting and stopping it by hand with the Tracing API. The modes that decide which runs keep a trace are values of the trace option, and those modes are what get you the failing attempt.
Choose a trace mode that keeps the failing attempt
The mode the docs recommend for CI throws away the one run you need when a test is flaky.
The docs recommend recording traces on CI on the first retry of a failed test, with trace: 'on-first-retry', which produces a trace.zip for each retried test. For a suite with flaky tests, that setup keeps the wrong run.
Anti-pattern
on-first-retry with flaky tests
// Anti-pattern: a flaky test's failed first run is never kept
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: 2,
use: {
trace: 'on-first-retry',
},
});
The docs' own retry table shows what goes wrong. When a test fails and then passes on retry, 'on-first-retry' keeps only the first retry's trace, and the failed first run has none. You open the report, click the trace and watch a test pass. On a pipeline with retries off, no test is ever retried, so no trace is kept at all.
Two other modes each fix half of the problem. With 'retain-on-failure', you get the failure, but the passing retry's trace is thrown away, and the passing retry is what you'd compare against. 'retain-on-first-failure' records a trace only for a test's first run, never for retries, and keeps it only if that run failed. If the retry fails for a different reason, that second failure leaves no trace.
'retain-on-failure-and-retries' keeps both. It records a trace for every run and keeps it for any run that failed or that is a retry. In the docs' retry table, a test that fails and then passes keeps the traces of the first run and the retry. When retries are on, this is the CI config we recommend:
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: 2,
use: {
// Keeps every failed run and every retry, so a flaky test leaves both sides
trace: 'retain-on-failure-and-retries',
},
});
If retries are off, there is no retry to keep, so 'retain-on-failure' does the same job. 'retain-on-failure' records a trace for every run, and so does 'retain-on-failure-and-retries'.
Keeping every failed run and every retry also means more trace.zip files in your CI artifacts. For a test that fails and then passes, 'on-first-retry' keeps only the first retry's trace, and 'retain-on-failure-and-retries' keeps the traces of the first run and the retry. A test that passes on its first run keeps no trace in either mode, so every extra file belongs to a test that failed. We'd pay for that storage. If artifact storage gets too big, keep traces for fewer days rather than for fewer runs: a mode that drops the failed attempt saves space by deleting the one file you needed.
The trace screenshots setting defaults to true, and we recommend leaving it on. Without screenshots the viewer has no timeline preview, and the timeline is how you spot the moment the page went wrong.
The --trace flag forces a mode for one run without touching the config. It accepts on, off, on-first-retry, on-all-retries, retain-on-failure, retain-on-first-failure and retain-on-failure-and-retries. Use it for a one-off CI rerun when you don't want to commit a config change:
npx playwright test --trace retain-on-failure
From the red CI job to an open trace
With the right mode, the trace exists. The first snag in opening it is how the artifact was packed.
If your CI uploads the HTML report as a .zip artifact, you can serve the archive as it is: show-report accepts a .zip downloaded from a CI artifact, as long as index.html is at its top level.
Anti-pattern
Zipping the report folder itself
Zipping the folder puts index.html one level down, and show-report can't serve the archive:
# Anti-pattern: the archive holds playwright-report/index.html, not index.html
zip -r playwright-report.zip playwright-report
Fix
Zip the folder's contents instead, so index.html sits at the root of the archive:
cd playwright-report
zip -r ../playwright-report.zip .
After you download the artifact, serve it:
npx playwright show-report playwright-report.zip
Once the report is served, click the trace icon next to the failing test's file name, and the trace opens.
A single trace.zip opens with the Playwright CLI or in the browser at trace.playwright.dev. Either way, give it the full path to the file. Remote traces open directly by URL, so you don't have to download the file from the CI run first. On trace.playwright.dev, you can pass the URL of a trace in accessible storage, such as your CI, as a query parameter:
https://trace.playwright.dev/?trace=https://ci.example.com/artifacts/1234/trace.zip
CORS rules may apply. If the viewer can't load the URL, download the file and open it by path instead.
The Trace Viewer docs describe the viewer's tabs, the Console tab among them. A red job needs only a few, read in the right order.
Locator timeouts: read the action log and the snapshots
A locator timeout is a symptom. The trace tells you what Playwright was still waiting for when time ran out.
Start where the viewer points you. When a test fails, the Errors tab shows its error messages, and the timeline shows a red line where the error occurred. Click the action at that line.
The Actions tab shows which locator each action used and how long each one took. Check two things there. First, is the locator the one you meant? A typo or a stale test ID shows up in the action list before anywhere else. Second, compare the failing action's duration with your timeout. An action that ran right up to the timeout spent that time waiting, so find out what it waited for.
Don't raise the action timeout the moment you see one. If the button is disabled because its data never loaded, a longer timeout only makes the job take longer to fail. Our checklist for debugging Playwright timeouts covers more timeout anti-patterns and what to do instead.
The log tells you what the action waited for. It shows what Playwright does behind the scenes: scrolling the element into view, waiting for it to be visible, enabled and stable, then performing the click, fill or press. Find the wait the log never got past:
- Visible: the element exists but is hidden, or the locator matched a different element that is hidden.
- Enabled: the button is disabled, for example because a form is still waiting for data or failing validation.
- Stable: the element is still moving, for example because of an animation.
The check that never passed is your root cause, not the timeout.
Then use the snapshots. Hover over the actions before the failure and watch the DOM snapshot change from one to the next. That is how you catch the step where a dialog opened or a list emptied. The Action snapshot is taken at the moment of the input, and it shows exactly where Playwright clicked. If that point lands on a cookie banner, a loading overlay or an element you didn't expect, you've found the problem without rerunning anything.
Backend errors: filter the Network tab to the failing action
Some locator timeouts aren't locator problems at all. The element never rendered because the request behind it failed.
Don't treat the missing element as a selector to fix. You rewrite the locator, the test fails again, and the list you were looking for still isn't there, because the API call that fills it returned an error. The UI shows only the result.
Traces record network activity next to the DOM snapshots, so the request that broke the page is in the same file. Use the timeline preview to find where the page stopped looking right. Then double-click the action just before the failure in the actions sidebar: the Network tab now shows only the requests made during that action. Click a request to see its request headers, response headers, request body and response body.
You're looking for a status the UI never showed you. A 401 means the request lacks valid authentication credentials for the requested resource. A 500 means the server encountered an unexpected condition that prevented it from fulfilling the request. A 200 means the request succeeded.
If the response is an error, fix the environment or the test data, not the locator. A locator changed to match the error state makes the test pass for the wrong reason.
Flaky tests: compare the failed attempt with the passing retry
A flaky test is a race, and one trace shows only one side of it.
Anti-pattern
Debugging from the passing retry's trace alone
The passing retry's trace records a run where the race went your way. Everything in it looks fine, because everything in it was fine.
Fix
With 'retain-on-failure-and-retries', a test that fails and then passes keeps the traces of both the first run and the retry. Open both, side by side.
Compare the Actions tab first. Line up the two action lists and look for the step whose duration differs most between the runs, or an action that ran in a different order. That step is where the race is.
Then compare that step's log and its network requests, filtered by double-clicking the action in each trace. Suppose the passing run's data request completes during the action before the click, while in the failed run the same request is still in flight when the click happens. The click then lands on a page that hasn't updated yet. The fix belongs in the test: wait for the state the click depends on, not for a fixed amount of time.
To get both traces on demand, rerun the suspect test with the mode forced from the command line. With retries set in the config, a failure followed by a pass keeps both traces:
npx playwright test tests/checkout.spec.ts --trace retain-on-failure-and-retries
Sharded runs: find the trace in the merged report
Sharding splits your tests into smaller parts called shards.
When the suite runs across shards, each shard has its own test report.
Anti-pattern
Downloading every shard's report to find one trace
With a separate report per shard, finding the trace means downloading each shard's artifact in turn until one of them has the test. Unless you already know which shard ran the test, don't go looking for it this way.
Fix
Build one report instead. A blob report contains every test that ran, its results and its attachments, traces included. Blob reports from several shards can be merged and converted into any other Playwright report. Have each shard produce a blob report, merge them into one HTML report in a final CI step, and upload that merged report as a single .zip with index.html at its root.
The sharding docs cover blob reports and merging, and our post on Playwright CI at scale walks through artifact handling for the blob reporter pattern in GitHub Actions.
Then open the merged artifact the same way as any other report:
npx playwright show-report merged-report.zip
The merged report holds the tests from every shard, so the failing test's trace icon is next to its file name, just as in an unsharded run.
When trace.playwright.dev is blocked or the trace holds secrets
The hosted viewer is convenient until your network won't let you reach it.
Trace Viewer loads the trace entirely in your browser and sends no data externally. That covers what happens to the trace, but the page itself still has to load, and a proxy that blocks the domain stops you before the trace is even read. When the hosted viewer won't load, use the Playwright CLI. It opens the same trace by full path or by URL:
npx playwright show-trace ~/Downloads/checkout-failure/trace.zip
npx playwright show-trace https://ci.example.com/artifacts/1234/trace.zip
What's inside the trace matters more than where you open it. A trace's Network tab shows each request's headers and body, and the Playwright CI docs warn that trace files can contain sensitive data like user credentials for a test user or access tokens to a staging backend. In a Playwright issue, a user asks whether a password entered in an input field while logging in can be hidden in the trace viewer.
Treat a trace from an authenticated run as a credential.
Anti-pattern
Trace URLs in a public channel
A trace uploaded to a public artifact store, with its URL pasted into a public channel, gives anyone who opens it whatever it holds.
Fix
The Playwright CI docs say to upload reports and traces only to trusted artifact stores, or to encrypt the files before upload. Keep trace URLs wherever you'd keep a token. If a test types a real password, switch it to a test account whose password can live in a trace.
For the rest of that problem, see how trace archives leak test data and how to stop it.
What the trace viewer won't tell you
A trace is evidence, and evidence has blind spots. Playwright issues show where users have run into them.
Near-misses. In a feature request, a user asks for traces to highlight actions based on how close they came to their timeout. Their reason: an action that takes 25 seconds with a 30 second timeout may pass, but is likely to become flaky when the environment is slower. You can check for near-misses yourself, because the Actions tab shows how long each action took. An action that passed just under its timeout today deserves the same reading as a failure.
Snapshots that don't match the page. A user reported that the trace viewer was not displaying elements for an Angular + Ionic app. In their report, the page content was not properly displayed in the Action, Before and After snapshots. Don't treat a DOM snapshot as an exact picture of the page. When the snapshot and the error disagree, trust the error and confirm with a rerun.
Several contexts in one test. In tests with several browser contexts, such as one user chatting with another, a user found it hard to tell which context an action belongs to. Keep multi-context tests short enough that the order of actions tells you who did what.
Formatted bodies. A user noted that the Network tab's built-in JSON pretty printer can lose precision for integers outside JavaScript's safe range, and that binary formats like protobuf can't be made readable. When a large ID or a binary payload looks wrong in a response body, check it against the backend's logs before you blame the backend.
Getting traces to the people who debug them
A trace only helps the person who opens it.
In the CI guide's GitHub Actions workflow, the HTML report is uploaded as the playwright-report artifact. A teammate downloads that artifact from the Artifacts section as a zip file, and serves it with npx playwright show-report. On trace.playwright.dev you can pass the URL of a trace stored in your CI as a query parameter, though CORS rules might apply. That works when one person debugs one failure. It gets slow when the failure comes up in a PR review or a bug report and several people need the same trace.
Currents lists every file of a run, traces included, on the run page's Attachments tab, where a type button filters the list to traces. Open trace opens a trace in the trace viewer, and Open test opens that test or that specific attempt on the run page.
A share link opens one test's steps, files and traces for anyone who has the link, without an account, until the link expires, so it can go in a PR comment or a bug report in place of an artifact URL.
The trace behind any link can contain sensitive data like user credentials for a test user or access tokens to a staging backend. Send it to the people who need it, not to a public channel.
A debugging order for every red job
- Keep the failing attempt. With retries on,
trace: 'retain-on-failure-and-retries'records a trace for every run and keeps it for any run that failed or that is a retry. If you don't enable retries but still want traces for failed tests, usetrace: 'retain-on-failure'. - Read the Errors tab and the red line on the timeline to find the action that failed.
- Read the action log and the snapshots to find the check that never passed and where the click landed.
- Filter the Network tab to the action before the failure to see whether the backend answered.
- Open the passing retry's trace, if there is one, and find the step that behaves differently.
Each anti-pattern above ends the same way: without the trace you need. A mode that never kept the failed run leaves nothing to read. A passing retry read as if it were the failure shows the run that worked. One report per shard hides the trace on a machine you have to guess. A trace in an untrusted store is one you shouldn't open or share.
Treat the trace as evidence, not a verdict. When a snapshot contradicts the error, or a step passed close to its timeout, rerun with the trace mode forced and confirm before you change the test.
Frequently asked questions
Which Playwright trace mode should I use in CI?
With retries on, use 'retain-on-failure-and-retries'. It keeps a trace for every failed run and every retry, so a flaky test leaves both the failure and the pass. With retries off, 'retain-on-failure' does the same job (how the modes compare).
Why does my Playwright trace show a test that passed?
You're probably using trace: 'on-first-retry'. It keeps only the first retry's trace, so when a flaky test fails and then passes on retry, the trace you open is the run that passed. The failed first run has no trace. Switch to 'retain-on-failure-and-retries' to keep both.
How do I open a Playwright trace from a CI artifact?
Download the HTML report artifact and serve it with npx playwright show-report playwright-report.zip, then click the trace icon next to the failing test. The archive needs index.html at its top level. A single trace.zip opens with npx playwright show-trace or at trace.playwright.dev, by path or by URL (details).
How do I debug a locator timeout with the trace viewer?
Click the failed action from the Errors tab and read its log. The log shows the checks Playwright ran before acting: visible, enabled and stable. The check that never passed is the root cause, and the Action snapshot shows where the click landed (the full walkthrough).
How do I find the cause of a flaky test in a trace?
Keep both traces with 'retain-on-failure-and-retries', then compare the failed attempt with the passing retry. Look for the action whose duration or order differs between the two runs. Then compare that action's log and network requests in each trace (how to compare them).
How do I find a trace in a sharded Playwright run?
Have each shard write a blob report, merge the blob reports into one HTML report in a final CI step, and upload that report as a single artifact. The merged report holds every shard's tests, so the failing test's trace is one click away, as in an unsharded run (details).
Is it safe to share a Playwright trace?
Treat a trace from an authenticated run as a credential. Its Network tab shows request headers and bodies, which can include a test user's credentials or an access token for a staging backend. Upload traces only to trusted artifact stores, or encrypt them first (details).




