ScreenshotNeo

BlogGuides

What Is Headless Mode in Browser Testing?

Headless mode runs browser automation without a visible window. Learn how it works, when to use it, how it differs from headed runs, and how to debug CI failures.

By the ScreenshotNeo team1 October 20268 min read

Headless mode runs a browser without displaying its normal user interface. An automation framework still launches a real browser process, creates pages, clicks elements, enters text, reads responses, takes screenshots, and reports results. The browser window is simply not shown. This makes headless execution practical on servers, containers, and continuous integration (CI) systems where no desktop session exists.

Chrome describes Headless mode as running Chrome in an unattended environment without a visible user interface. Modern Chrome Headless uses the same browser implementation as headed Chrome, but browser frameworks can also use separate headless builds. That implementation detail can affect rendering and behavior, so “headless” does not guarantee that every configuration matches every headed run.

How headless browser testing works

  1. Your test runner starts a browser process with headless enabled.
  2. The browser creates an isolated context or profile.
  3. Your test code opens a page and performs actions through an automation protocol such as Chrome DevTools Protocol or WebDriver.
  4. The page loads JavaScript, stylesheets, fonts, images, and other resources just as an automated browser would in a visible session.
  5. Assertions inspect the DOM, network responses, accessibility tree, console output, or screenshots.
  6. The runner closes the browser and returns an exit status to CI.

Headless changes visibility, not the testing API. Playwright, Puppeteer, Selenium, and ChromeDriver expose nearly the same page operations in headed and headless runs. The differences that matter are the browser binary, channel, operating system, graphics stack, viewport, fonts, permissions, and launch flags.

Headless versus headed mode

Concern Headless Headed
Window No visible browser window Window is displayed
Typical use CI, containers, servers, scheduled jobs Local debugging and visual inspection
Automation Fully automated Can be automated or manually observed
Debugging Use traces, screenshots, video, logs, or remote debugging Watch the page and use normal developer tools
Linux display requirement Usually no X server Requires a display; CI commonly uses Xvfb
Rendering parity Depends on browser build and channel Depends on browser build and desktop environment

Use headless mode for repeatable unattended checks. Switch to headed mode when you need to see an interaction, inspect a layout problem, reproduce a user-flow issue, or use browser developer tools. On Linux CI, Playwright documents running headed tests with xvfb-run; its Docker image and GitHub Action include Xvfb.

Implementation differences you must account for

Modern Chrome Headless

Chrome’s current Headless mode shares the browser implementation used by headful Chrome. Chrome documents automation through Puppeteer and ChromeDriver/WebDriver, and Headless supports screenshots, PDF generation, remote debugging, and virtual-screen configuration. A typical unattended setup pins a Chrome for Testing binary and drives it with an automation tool.

Playwright’s headless browser choices

Playwright supports Chromium, Firefox, WebKit, Google Chrome, and Microsoft Edge channels. Its default Chromium headless setup may use a separate headless shell. Selecting the chromium channel opts into the newer Chromium mode. Playwright warns that the shell and Chrome/Edge’s newer headless implementation can differ, so choose the channel that matches the browser behavior you need to test.

For regression tests against a publicly shipped browser, run the branded Chrome or Edge channel when that is your production target. For broad engine coverage, include Chromium, Firefox, and WebKit in the test matrix. Keep the browser versions and operating-system images pinned so a browser update does not silently change screenshots or selectors.

Run a headless test with Playwright

Playwright launches browsers headlessly by default. Install it, create a test, and run the test from your project directory.

npm init playwright@latest
npx playwright test

The following test checks a page title, captures a screenshot, and records a trace when it fails:

import { test, expect } from '@playwright/test';

test('home page loads', async ({ page }) => {
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
  await expect(page).toHaveTitle(/Example Domain/);
  await page.screenshot({ path: 'artifacts/home.png', fullPage: true });
});

Run the same test visibly while debugging:

npx playwright test --headed

On a Linux CI worker without a desktop session:

xvfb-run -a npx playwright test --headed

For browser-launch diagnostics, enable Playwright’s browser log:

DEBUG=pw:browser npx playwright test

In playwright.config.ts, make the execution choices explicit:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  reporter: [['html', { outputFolder: 'artifacts/report' }]],
  use: {
    baseURL: 'https://example.com',
    headless: true,
    browserName: 'chromium',
    channel: undefined,
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    viewport: { width: 1440, height: 900 },
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

Use channel: 'chromium' when you specifically want Playwright’s newer Chromium channel behavior. Use a branded chrome or msedge channel when your release target is that installed browser and your environment has it available.

Run Chrome Headless from the command line

Chrome’s command-line mode is useful for a quick capture or for validating that a container can launch Chrome at all. The exact binary path depends on your installation.

google-chrome \
  --headless \
  --disable-gpu \
  --no-sandbox \
  --window-size=1440,900 \
  --screenshot=home.png \
  https://example.com

Only use --no-sandbox when your container policy requires it and you understand the isolation trade-off. Prefer a correctly configured sandboxed browser user when possible. For repeatable test suites, a framework such as Playwright or Puppeteer gives you better waits, isolation, traces, and failure reporting.

What to configure for reliable headless tests

  • Viewport: Set width and height explicitly. Responsive breakpoints can otherwise change between local and CI runs.
  • Device scale factor: Choose a fixed scale when comparing screenshots.
  • Browser channel: Pin Chromium, Chrome, Firefox, or WebKit versions and use the same channel in CI.
  • Locale and timezone: Set them when dates, currency, number formatting, or translated text are asserted.
  • Fonts: Install the same font packages in every runner. Missing fonts cause layout and screenshot differences.
  • Network waits: Prefer locator-based readiness checks over arbitrary sleeps. Use networkidle only when the application’s background traffic makes it appropriate.
  • Authentication: Reuse a tested storage state or create a fresh context per test. Never put credentials in source control.
  • Permissions: Grant only the camera, microphone, geolocation, or notification permissions a test needs.
  • Artifacts: Retain the HTML report, trace, console log, network log, screenshot, and video for failures.

Common headless errors and fixes

Error or symptom Likely cause Fix
Browser fails to launch in a container Missing shared libraries, incompatible browser binary, or insufficient sandbox permissions Use the framework’s supported browser image, install its dependencies, and pin the browser version. Inspect DEBUG=pw:browser output.
“No display” or X server error A headed run is being attempted on a Linux worker without a display Run headless, or wrap the headed command with xvfb-run -a.
Tests pass headed but fail headless Timing race, viewport difference, missing font, browser-channel difference, or code that depends on a visible window Set viewport and fonts, replace sleeps with locator waits, compare channels, and inspect a failure trace and screenshot.
Element is not visible or clickable Overlay, animation, cookie dialog, lazy rendering, or the element is outside the viewport Wait for the locator, close the overlay, scroll into view, and assert visibility before clicking.
Flaky timeout Slow dependency, never-ending network request, or an overly short timeout Wait for a specific application-ready signal, mock unstable dependencies where appropriate, and set separate action and navigation timeouts.
Screenshot differs across machines Different browser version, OS, font, viewport, device scale, timezone, or animations Standardize the runner image and browser, disable animations for visual tests, and set all environment-dependent options.
Blank page or incomplete content JavaScript error, blocked resource, consent wall, authentication redirect, or capture taken too early Check console and network logs, wait for a meaningful selector, verify credentials, and capture after the application is ready.
Tests hang at shutdown Open WebSocket, server, child process, or browser context remains active Close contexts and browsers in teardown and ensure test servers receive a shutdown signal.

Performance, reliability, and cost

Performance

Headless mode avoids drawing a visible window, but the expensive parts of a test are usually page JavaScript, network requests, image decoding, and browser startup. Reuse a browser process while creating isolated contexts, run independent tests in controlled workers, and avoid loading resources that are irrelevant to the assertion. Do not claim a fixed speed improvement without measuring your own pages and runner.

Reliability

Reliability comes from deterministic inputs: pinned browser and OS images, fixed locale and timezone, stable test data, explicit readiness checks, isolated contexts, and retained failure artifacts. Use retries only for diagnosing transient infrastructure failures; retries should not hide a deterministic product defect. A test that passes only after a retry needs investigation.

Cost

Self-hosted headless tests consume your CI minutes, compute, storage for artifacts, and maintenance time for browser dependencies. Parallel workers reduce wall-clock time but increase peak CPU and memory. Keep video and traces for failures or retries rather than every successful test when artifact storage is a concern.

Headless screenshots without maintaining a browser

If your goal is a clean page image rather than an end-to-end assertion, ScreenshotNeo provides a website screenshot API and MCP server. The API accepts one GET request and returns PNG, JPEG, WebP, or PDF output. Its capture options include full-page screenshots with lazy images loaded, CSS-element capture, dark mode, device presets or custom viewports, retina scale, custom CSS and JavaScript, selector waits, delays, network-idle waits, request and resource blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture, and PDF settings.

Or skip the browser setup

Use the same capture from cURL, Python, or Node.js. See the ScreenshotNeo API documentation for the available parameters.

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,
)
r.raise_for_status()
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}`);

ScreenshotNeo accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed. Every response includes X-Page-Verdict and X-Billed headers so your pipeline can see what happened. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account and start with 1,000 screenshots each month at no charge.

FAQ

Is headless mode a different browser?

Not always. Modern Chrome Headless shares Chrome’s browser implementation, while Playwright’s default Chromium headless configuration can use a separate headless shell. Check the framework and channel you run.

Can headless mode take screenshots and PDFs?

Yes. Chrome documents screenshots, PDF generation, remote debugging, and virtual-screen configuration for Headless mode. Automation frameworks expose equivalent APIs.

Should every CI test run headless?

Usually, yes for unattended Linux workers. Keep a headed reproduction path for failures that require visual inspection, using a local desktop or Xvfb.

Does headless mode hide browser errors?

No. Capture console messages, page errors, network failures, traces, screenshots, and videos so failures remain diagnosable without a visible window.

When should I use a screenshot API instead of Playwright?

Use a test framework when you need assertions, multi-step workflows, or cross-browser coverage. Use a screenshot API when you need repeatable page images or PDFs without maintaining browser binaries and CI display dependencies.