How to Debug Websites in a Headless Browser
Debug headless browser failures with Playwright Inspector, traces, logs, network evidence, and a repeatable CI workflow.
Short answer: reproduce one failing action, then inspect the evidence around that moment: the locator and DOM snapshot, actionability log, browser and test console, network requests, and a screenshot or trace. Use Playwright Inspector for an interactive local failure, a headed run when you need to see rendering, and Trace Viewer when the failure happened in CI. Keep the original headless run as the final check.
Playwright runs browsers headless by default. The workflow below shows how to debug a page without guessing at timing or changing several variables at once.
1. Read the failure before changing the environment
- Record the assertion message, expected value, received value, call log, and source line.
- Choose one failing test and one action. A narrow reproduction makes page state and request sequences easier to correlate.
- Note the browser, viewport, URL, test data, and whether the failure is local or in CI.
2. Reproduce interactively with Playwright Inspector
Run one test in debug mode:
npx playwright test tests/checkout.spec.ts:24 --debug
The Inspector opens a headed browser and lets you step through actions, edit or pick locators, and view actionability logs. A locator that fails because an element is hidden, moving, covered, or not yet attached is different from a selector that never matches. See the Playwright debugging guide.
Make a normal launch visible
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: false, slowMo: 150 });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.pause();
await browser.close();
headless: false changes rendering and interaction conditions. A visible run helps observation but does not prove that a headless or CI failure is fixed. Re-run with the original launch settings.
3. Capture a trace for failures you cannot watch live
Tracing preserves a time-ordered record for inspection after a run. It includes action details, source locations, DOM snapshots, errors, console messages, network requests, and screenshots when recorded. Playwright documents traces as useful for CI failures.
import { test, expect } from '@playwright/test';
test('checkout', async ({ page }) => {
await page.goto('https://example.com/checkout');
await page.locator('[data-testid="pay"]').click();
await expect(page.getByRole('heading', { name: 'Receipt' })).toBeVisible();
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({ use: { trace: 'retain-on-failure' } });
npx playwright show-trace path/to/trace.zip
In Trace Viewer, step through the action timeline. For the failed action, compare the DOM snapshot before and after it, read the action log, then check matching console and network events. A screenshot proves what was visible; the snapshot, console, and request details help explain why.
4. Correlate page, browser, and network evidence
| Symptom | Evidence | Direction |
|---|---|---|
| Locator or click fails | Actionability log, locator, DOM snapshot | Selector drift, hidden or covered element, wrong frame, or timing |
| Page looks wrong | Before/after snapshots and screenshots | Layout state, viewport, fonts, flags, or rendering race |
| Data or assets are missing | Requests, responses, console output | Blocked request, authorization, CORS, or server error |
| Browser stalls or will not launch | API and browser launch logs | Executable, dependency, sandbox, or resource problem |
| Only CI fails | Trace from the failing job | Environment, timing, data, or network difference |
Do not infer a root cause from one screenshot or timeout. Correlate the failed action with state and requests at the same timestamp.
5. Turn on verbose Playwright logs
DEBUG=pw:api npx playwright test tests/checkout.spec.ts:24
For an early browser launch problem, Playwright’s CI guidance documents:
DEBUG=pw:browser npx playwright test tests/checkout.spec.ts:24
Debug namespaces and commands can change with framework versions. Check the installed version’s official documentation before copying old launch flags.
6. Add targeted diagnostics to a test
import { test } from '@playwright/test';
test('diagnostic capture', async ({ page }, testInfo) => {
page.on('console', msg => console.log(`[console:${msg.type()}] ${msg.text()}`));
page.on('pageerror', error => console.error('[pageerror]', error));
page.on('requestfailed', request => console.error('[requestfailed]', request.url(), request.failure()?.errorText));
page.on('response', response => { if (response.status() >= 400) console.error('[http]', response.status(), response.url()); });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.screenshot({ path: testInfo.outputPath('state.png'), fullPage: true });
});
Keep listeners focused on the failing test. Logging every successful request on a large page can obscure the event that matters.
7. Debug common headless-only differences
- Viewport and device: set an explicit viewport and device scale factor. Responsive breakpoints can change which locator exists.
- Fonts and rendering: wait for
document.fonts.readywhen text measurement affects layout and ensure CI has required fonts. - Animations: disable them for diagnosis, then verify with production timing restored.
- Frames: inspect
page.frames()and target the correct iframe. - Authentication: confirm identical storage state, cookies, headers, and flags locally and in CI.
- Time and locale: set timezone and locale explicitly when date formatting affects assertions.
- Network readiness:
networkidledoes not guarantee that application data is ready. Prefer a response assertion or meaningful selector.
8. A repeatable CI procedure
- Save the failed test’s trace, video or screenshot artifacts, console output, and framework logs.
- Open the trace from the failing job before reproducing locally.
- Find the first divergence: an unexpected request, console error, missing DOM node, or actionability failure.
- Reproduce with the same browser project, viewport, storage state, and test data.
- Make one change, run the focused test headless, then run the wider suite.
- Keep trace retention sized to your CI storage budget.
9. Performance, reliability, and cost considerations
- Use a focused test and line filter while investigating.
- Trace and video artifacts consume storage; retain them on failure unless every passing run is required.
- Headed mode and
slowMoare observation tools, not performance measurements. - Prefer deterministic waits tied to application state. Large fixed delays hide races and slow runs.
- Record browser version and OS image with CI artifacts.
- When a third-party dependency is flaky, capture URL, status, and timing before deciding whether a test-owned stub is appropriate.
10. Puppeteer users
Puppeteer has its own official debugging workflow, including headed launches and Node/browser debugging tools. Exact APIs depend on the installed version; follow the Puppeteer debugging guide. The evidence model remains the same: reproduce one action, inspect page state, correlate console and network output, and preserve CI artifacts.
Or skip the browser setup
If your goal is a clean image of a page while diagnosing visual state, ScreenshotNeo provides a single capture request. It accepts the consent banner like a visitor, removes more than 60 known consent platforms plus newsletter popups and chat widgets before the shot, and lets each step be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and whether the shot was billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI clients.
See the ScreenshotNeo API docs for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Every plan includes full-page or CSS-element capture, dark mode, device presets or custom viewport, retina scale, PDF controls, custom CSS and JavaScript, clicks and waits, blocking rules, headers, cookies, user agent, authorization, timezone, geolocation, transparent background, resizing, cache TTL, signed links, async webhooks, bulk capture, usage API, and OpenAPI support. Free includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
Troubleshooting checklist
| Error or symptom | Cause to check | Fix |
|---|---|---|
Timeout exceeded |
Wrong state, slow dependency, or broad wait | Inspect trace and requests; wait for a meaningful selector or response. |
Element is not visible |
Responsive layout, overlay, animation, or wrong frame | Use Inspector picker and inspect the snapshot. |
| Selector matches zero nodes | Selector drift or page not reached | Check URL, DOM snapshot, and navigation response. |
| Requests fail in CI | Credentials, proxy, DNS, CORS, or blocked third party | Compare network and console evidence from CI with local output. |
| Browser executable missing | Browser binaries or OS dependencies absent | Install the framework-required version and inspect DEBUG=pw:browser. |
| Headed passes, headless fails | Mode changed timing, viewport, GPU, or environment | Use headed mode for observation, then verify under original headless conditions. |
FAQ
Should I start with a screenshot or a trace?
Start with a trace when the failure involves an action, timing, or CI. Add a screenshot when visual state is the question.
Does networkidle mean the page is ready?
No. It describes observed network activity. Assert the application state your test needs.
Can a headed success prove a headless bug is fixed?
No. Re-run the same headless project and environment.
What should be retained from CI?
Keep the failing trace, relevant screenshot or video, console and framework logs, browser version, and test configuration.


