TL;DR
- Playwright debugging means finding out why a test fails, whether the cause is a wrong selector, a timing issue, an unexpected navigation, or bad app state, by getting visibility into what the browser actually did.
- Use the Inspector and headed mode to debug failures you can reproduce locally, and the Trace Viewer to diagnose the headless CI failures you can't reproduce.
- Verbose logs, Chrome DevTools, and timeout tuning turn a vague "timed out" error into a specific, fixable cause.
- Browserless hosts the browsers and adds live debugging, session replay, and recording for when local debugging stops matching production.
Introduction
Playwright is great at driving browsers, but when a test fails, it can still feel like guesswork. Maybe a locator is matching the wrong element. Maybe the app is rendering late. Maybe it only breaks in CI, where you get a red X and a log line that says timed out.
The good news is that Playwright ships with a full Playwright debugger toolkit. You can stop mid-test, step through actions, inspect the DOM, replay failures after the fact, and crank up internal logs when the framework is waiting on something you can't see.
In this guide, you'll learn how to debug Playwright tests using the Inspector, Trace Viewer, the VS Code debugger, verbose logging, headed mode, Chrome DevTools, and a few patterns that make flaky tests reproducible. We'll finish by looking at what changes when debugging happens remotely at scale.
What is Playwright debugging?
Playwright debugging is the process of finding out why a browser automation test fails: missing elements, wrong selectors, timing issues, navigation surprises, or the app being in an unexpected state. In practice, it's less about rerunning the same test file and more about getting visibility into what the browser window actually did, step by step.
The Playwright debugger tools available
Once you treat Playwright debugging as a visibility problem, the tooling clicks. Playwright gives you a GUI tool for stepping through actions, a trace system for post-mortems, a tight VS Code integration, and low-level logs that show what Playwright is waiting for internally.
Playwright Inspector
If you want the fastest path from failure to understanding, start with the Inspector.
Run this snippet:
npx playwright test --debug
That opens the Playwright Inspector alongside a headed browser. From there, you can:
- Step through actions one at a time.
- See which locator Playwright is using for the current action.
- Live-edit the locator and immediately see which elements match.
- Use Pick Locator to generate a resilient selector and copy it back into your test code.
A useful detail: --debug also flips a couple of defaults that make debugging less painful. Browsers launch headed, and the default timeout becomes 0 (meaning no timeout), so you can poke around without fighting timers.
When you're debugging a specific test line, run just that test:
npx playwright test example.spec.ts:10 --debug
Or pin it to a single project (browser profile):
npx playwright test --project=chromium --debug
If you don't want to step through the whole test execution to reach the interesting part, drop in a manual breakpoint:
await page.pause();
Then run in debug mode and hit Resume when you're ready.
Trace Viewer
The Inspector is perfect when you can reproduce locally. Trace Viewer is for the cases you can't – especially headless CI failures.
A Playwright trace is basically a flight recorder. It acts as a timeline of actions, DOM snapshots, screenshots, network requests, console logs, source code mapping, and actionability details.
To record a trace locally:
npx playwright test --trace on
npx playwright show-report
To open a specific trace archive:
npx playwright show-trace path/to/trace.zip
You can also open it in the browser via trace.playwright.dev. One key security feature is that the trace loads entirely in your browser and does not transmit the trace data elsewhere.
In CI, the sweet spot is usually tracing only when something fails, without generating giant artifacts for every run. Playwright recommends recording a trace on the first retry:
// playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
retries: 1,
use: {
trace: "on-first-retry",
},
});
That produces a trace.zip for the retried test, which is exactly when you need it.
VS Code debugger
If you live in VS Code, you can keep your whole Playwright debugging loop in the editor.
With the Playwright VS Code extension, you can:
- Set breakpoints in a test file and step like normal Node debugging.
- Run a single test or a whole project from the testing sidebar.
- Use locator picking, so you're not hand-authoring selectors from memory.
If you run tests with Show Browser enabled, the browser session is reused, which makes it much easier to keep Chrome DevTools open while you iterate.
Verbose logging
Sometimes the UI tools tell you what happened, but not why Playwright made a decision. That's when verbose logging pays off.
The most common starting point is pw:api:
DEBUG=pw:api npx playwright test
You'll see Playwright's internal action logs, including what it's waiting for and which actionability checks are blocking an interaction. That output goes to stderr by default.
Verbose logs are especially good at exposing problems that look like timing bugs but are actually:
- A locator resolving to multiple elements in strict mode.
- An overlay stealing clicks.
- A navigation happening earlier than you thought.
- A "stable" check failing because layout is shifting.
Now that you've got the main tools in your pocket, the next decision is which execution mode to debug in: headed or headless.
Headed vs. headless: when to use each for Playwright debugging
Most CI runs are headless because it's faster and simpler to run at scale. The trade-off is obvious: you lose visual context. For Playwright debug work, you usually start headed locally, then move back to headless once the test is stable.
To run headed from the CLI:
npx playwright test --headed
Or in config:
// playwright.config.ts
export default {
use: {
headless: false,
},
};
If the failure smells like a timing issue, slowing the test down can make it visible. You can do that with slowMo in launch options:
export default {
use: {
launchOptions: {
slowMo: 50,
},
},
};
Headed mode is also where locator debugging is simplest, because you can see misclicks, sticky headers, hidden buttons, and animations that a log line won't explain.
A visible browser window gets you close, but Chrome DevTools is what lets you inspect the page at the level frontend bugs actually live.
Using Chrome DevTools in Playwright
Chrome DevTools is still the best microscope for modern web apps: DOM state, network, console errors, storage, and performance. The trick is wiring it into a Playwright run without losing your place.
A reliable workflow looks like this:
- Add a breakpoint:
await page.pause(); - Run with DevTools-friendly debug mode
- Inspect the page like you would in normal frontend debugging
Playwright also supports a console-focused debug mode:
PWDEBUG=console npx playwright test
When you do this step, a playwright object becomes available in the DevTools console, which can help with selector debugging using Playwright's own selector engines.
A few practical DevTools moves during a Playwright debugging session:
- Elements panel – confirm the element you think you're clicking is actually the one receiving events.
- Network tab – verify API calls, status codes, redirects, and caching behavior.
- Console – look for client-side exceptions or failed resource loads that never surface in the test output.
- Application tab – inspect cookies, localStorage, sessionStorage when auth state is involved.
If you need to prove a theory mid-test, page.evaluate() is the quick scalpel:
const isBannerVisible = await page.evaluate(() => {
return Boolean(document.querySelector('[data-testid="banner"]'));
});
Once you can inspect state and network behavior, you're set up to tackle the hardest class of failures: flaky tests.
How to debug flaky Playwright tests
A normal failure is a one-off. Flaky tests are worse because they teach you the wrong lesson: you rerun until green, then the pipeline breaks again tomorrow.
The common causes tend to cluster:
- Timing issues – the UI is correct, but your test is ahead of the app.
- Selector instability – DOM changes, multiple matches, or the wrong element becomes clickable.
- Shared state – tests leak auth, localStorage, server data, or seeded records into each other.
- Network variability – slow APIs, retries, rate limits, or region-specific behavior.
A few Playwright debugging techniques that actually reduce flakiness instead of masking it:
- Prefer locator-driven waits over manual sleeps. If you're using
waitForTimeout()as a crutch, you're usually hiding a missing condition. - Use
expect()assertions as synchronization points for UI state, not just validation. - Keep tests isolated – avoid reusing stateful contexts across unrelated tests unless you're doing it intentionally.
When a selector is suspicious, make it visual. Playwright has a locator.highlight() method you can call to confirm what your locator resolves to:
const saveButton = page.getByRole("button", { name: "Save" });
await saveButton.highlight();
await saveButton.click();
This is a fast sanity check when you suspect strictness or dynamic DOM issues.
Finally, use traces to find patterns across runs. If a test fails 1 in 20 times, record traces on retry and compare them side-by-side. The delta is usually something real, like a slow request, an animation not finished, or an element covered by an overlay.
Flaky tests also show up most painfully in CI, so what about Playwright debugging when you don't have a local browser window at all?
Playwright debugging in CI
CI failures feel opaque because you're missing three things at once: a visible browser, full logs, and an interactive debugger. The fix is to treat CI as an artifact factory.
Start with traces on failure, or on-first-retry, then make sure the trace archives and HTML report are saved as build artifacts. Playwright's Trace Viewer is designed for exactly this workflow.
A minimal CI-friendly setup often includes:
retries: 1so you get a retry run worth tracing.use.trace: 'on-first-retry'so traces exist when you need them.- An HTML report you can open locally or publish somewhere internal.
Playwright's reporters are configurable, and by default the HTML report is written to playwright-report/. If you need a custom path, you can set PLAYWRIGHT_HTML_OUTPUT_DIR or configure the reporter in playwright.config.
If local and CI behavior diverge, assume environment differences first:
- Headless vs. headed defaults.
- CPU and memory pressure.
- Network latency.
- Different browser versions or fonts.
With traces and reports flowing out of CI, timeouts are usually the next thing you'll be staring at, since that's where many of these failures surface.
Debugging Playwright timeouts
A timeout error doesn't mean Playwright is slow. It means Playwright waited for a condition and never saw it become true. That condition might be an element becoming actionable, a navigation completing, or an assertion eventually passing.
In Playwright Test, these are the timeout knobs you'll touch most often:
| Timeout type | What it covers | Common fix |
|---|---|---|
| Test timeout | Total time for a test (default 30s) | Make the test faster, or raise per-test when doing heavy setup |
| Expect timeout | How long an assertion waits (default 5s) | Assert the right thing, or pass { timeout } for slow UI state |
| Action or operation timeouts | Click, fill, waits, and many page operations | Fix state and waiting, then tune setDefaultTimeout() if needed |
| Navigation timeouts | goto, waitForURL, navigations | Fix load conditions or tune setDefaultNavigationTimeout() |
A few concrete patterns help you tell tuning apart from real failures:
- If an element never appears, raising timeouts just makes you wait longer to fail.
- If the element appears but isn't clickable, the bug is usually actionability: it's covered, disabled, or moving.
- If navigation timeouts happen in CI only, look for resource slowness or external dependencies that aren't stable.
Timeout configuration examples you'll actually use:
Global in config:
export default {
timeout: 60_000,
expect: { timeout: 10_000 },
};
Per test:
test("imports data", async ({ page }) => {
test.setTimeout(120_000);
// ...
});
Per assertion:
await expect(page.getByText("Ready")).toBeVisible({ timeout: 20_000 });
And if you're debugging a navigation-heavy flow, it can help to separate navigation timeout from general action timeout with page.setDefaultNavigationTimeout() or context equivalents.
Once you can read timeouts as signals instead of noise, you're ready for the final step: debugging Playwright tests when the browser isn't running on your laptop at all.
Remote Playwright debugging at scale with Browserless
Local Playwright debugging is great until it stops matching reality. The moment you need scale, regional fidelity, or an environment that behaves like production, you end up spending more time managing browsers than debugging tests.
Browserless solves that by hosting browsers as a service. You keep your Playwright scripts, but connect to a managed browser over WebSocket. For Playwright, the quick start path is chromium.connectOverCDP() against a regional endpoint.
A minimal connection looks like this:
import { chromium } from "playwright-core";
const browser = await chromium.connectOverCDP(
`wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_TOKEN}`,
);
try {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("https://www.example.com/");
} finally {
await browser.close();
}
Browserless supports multiple regions with load balancers, so you can run the same test from London or Amsterdam endpoints and debug against realistic latency and regional routing.
Where this gets interesting for debugging is the visibility layer Browserless adds around your sessions:
- LiveURL – call
Browserless.liveURLto get a shareable link to your running session, so you can watch and interact with the remote browser in real time. - Session Replay – record DOM mutations, clicks, scrolling, keyboard input, console logs, and network requests, then review them later in the dashboard.
- Screen recording – capture sessions as WebM video when you need pixel-level playback of what happened.
One pragmatic workflow is to keep using Playwright traces for step-by-step test internals, while using Browserless replay or video to see the user perspective of the same run – A combination is especially useful when you're debugging failures you can't reproduce locally, or when CI machines behave differently under load.
Browserless also supports Playwright beyond Chromium. For Firefox and WebKit, you'll typically use the native connect method rather than CDP, since CDP is Chrome-only.
From here, the last step is stitching everything together into a debugging routine you'll actually use day to day.
Conclusion
Playwright debug work gets dramatically easier once you stop treating failures as log-reading exercises. Run with the Inspector when you can reproduce locally, record traces so CI failures come with a timeline, use DevTools when you need the frontend microscope, and then tighten up flakiness by making waits explicit and selectors resilient.
If your biggest debugging problems come from scale, regional differences, or browser instability in CI, BaaS gives you managed browsers plus a visibility layer with live debugging, replays, and recordings. You still own the Playwright scripts, but you spend less time fighting infrastructure and more time fixing the test. You can sign up for free and connect your existing tests over WebSocket.
Playwright debug FAQs
What is the Playwright Inspector and how do I use it?
The Playwright Inspector is the GUI tool that lets you step through test execution, inspect and edit locators, and see actionability logs. The quickest way to open it is:
npx playwright test --debug
You can also drop await page.pause(); into a test to stop exactly where you need to inspect state.
How do I debug a Playwright test in headed mode?
Run the test with the --headed flag:
npx playwright test --headed
Headed mode is usually the best starting point for Playwright debugging because you can see click targets, overlays, animations, and focus issues that are invisible in headless mode.
What's the difference between Playwright timeout types?
In Playwright Test, the common ones are:
- Test timeout – total time budget for a test (default 30s).
- Expect timeout – how long assertions wait for conditions (default 5s).
- Action and navigation timeouts – how long operations like clicks and navigations can wait.
They're configured in different places, and raising them blindly can hide real failures.
How do I use Chrome DevTools with Playwright?
Add await page.pause(); where you want to stop, then run with:
PWDEBUG=console npx playwright test
This code makes a playwright object available in the DevTools console, which helps when you're debugging selectors and page state using Playwright's selector engines.
How can I debug Playwright tests running remotely in the cloud?
Connect Playwright to a remote browser over WebSocket, then rely on artifacts and replayability:
- Playwright traces for step-by-step test internals.
- Browserless LiveURL for watching and interacting with the session in real time.
- Browserless Session Replay or screen recording to understand and play back what happened.
Browserless connects via CDP for Chromium with connectOverCDP(), and offers regional endpoints so you can debug against realistic latency and regional routing.