Headless Chrome: What It Is and How to Use It for Testing

Headless Chrome: What It Is and How to Use It for Testing

Headless Chrome runs the Chrome browser without a visible UI. It's the default mode for browser automation in CI/CD pipelines because it's faster, uses less memory, and works on servers with no display. This guide covers how to run headless Chrome with Playwright, Puppeteer, and Selenium — plus how to configure it for Docker and GitHub Actions.

Key Takeaways

Headless is the default in CI. You almost never want to install a display server on a CI machine just to run tests. Headless Chrome gives you full browser capabilities without the display dependency.

Playwright uses headless mode by default. You don't need to configure anything — your tests run headlessly unless you pass headless: false for local debugging.

Chrome has two headless modes. The old --headless flag uses Chrome's legacy headless implementation. The new --headless=new (default since Chrome 112) is a full Chrome instance without a window — more compatible and less quirky.

Headless Chrome can do everything headed Chrome can. JavaScript execution, cookies, localStorage, WebGL, WebRTC, service workers, file downloads — all work the same. The only difference is there's no window on screen.

Some issues only appear in headed mode. Hover states, tooltips, drag-and-drop, and certain viewport-dependent behaviors can differ between headless and headed. Always run your full suite in both modes before shipping.

What Is Headless Chrome?

Headless Chrome is Chrome running without a graphical user interface. The browser loads pages, executes JavaScript, handles cookies and sessions, and renders content — exactly like normal Chrome — but without displaying anything on screen.

The term "headless" comes from the idea of a browser without a "head" (the visible window). The browser process runs and does its work invisibly.

Why Use Headless Chrome?

CI/CD pipelines: Most CI servers (GitHub Actions, GitLab CI, Jenkins) don't have displays. Headless Chrome runs there without needing a virtual framebuffer like Xvfb.

Speed: Without rendering to a display, tests run slightly faster and use less CPU.

Memory: No GPU process, no compositing — headless Chrome uses less memory than headed mode.

Parallel execution: You can run many headless Chrome instances simultaneously without display conflicts.

Headless vs Headed Mode

Headless Headed
Visible window No Yes
CI servers Yes Needs Xvfb
Speed Faster Slightly slower
Memory Less More
Debugging Harder Easier
Render fidelity Near-identical Identical
Font rendering Sometimes differs Exact

Running Headless Chrome with Playwright

Playwright uses headless mode by default. You don't need to do anything special.

import { chromium } from 'playwright';

// Headless by default
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
const title = await page.title();
console.log(title);
await browser.close();

Switch to Headed Mode (for debugging)

// Show the browser window
const browser = await chromium.launch({ headless: false });

Or via environment variable:

PWHEADLESS=false npx playwright <span class="hljs-built_in">test

Or in playwright.config.ts:

export default defineConfig({
  use: {
    headless: false, // show browser during test run
  },
});

Playwright Test Runner

# Headless (default)
npx playwright <span class="hljs-built_in">test

<span class="hljs-comment"># Headed — shows browser window
npx playwright <span class="hljs-built_in">test --headed

<span class="hljs-comment"># With slow-motion for debugging
npx playwright <span class="hljs-built_in">test --headed --slowMo 500

Viewport and Screen Size

In headless mode, you should set an explicit viewport to avoid size-dependent failures:

const browser = await chromium.launch();
const page = await browser.newPage();
await page.setViewportSize({ width: 1280, height: 720 });

In playwright.config.ts:

export default defineConfig({
  use: {
    viewport: { width: 1280, height: 720 },
  },
});

Running Headless Chrome with Puppeteer

Puppeteer is Google's browser automation library, built specifically around Chrome DevTools Protocol.

const puppeteer = require('puppeteer');

// Headless (new mode, default since Puppeteer v21)
const browser = await puppeteer.launch();

// Headless (old mode — more compatible with some tools)
const browser = await puppeteer.launch({ headless: 'shell' });

// Headed
const browser = await puppeteer.launch({ headless: false });

Basic Puppeteer Headless Example

const puppeteer = require('puppeteer');

async function run() {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.setViewport({ width: 1280, height: 800 });
  await page.goto('https://example.com', { waitUntil: 'networkidle0' });

  const title = await page.title();
  console.log('Title:', title);

  await page.screenshot({ path: 'screenshot.png' });
  await browser.close();
}

run();

Puppeteer Chrome Arguments

const browser = await puppeteer.launch({
  args: [
    '--no-sandbox',              // required in some Linux/Docker environments
    '--disable-setuid-sandbox',  // required alongside no-sandbox
    '--disable-dev-shm-usage',   // prevents crashes in Docker (limited /dev/shm)
    '--disable-gpu',             // disable GPU (not needed in headless)
    '--window-size=1280,800',
  ]
});

Running Headless Chrome with Selenium

Selenium requires explicit configuration to run Chrome in headless mode.

Python

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument('--headless=new')   # new headless mode (Chrome 112+)
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--window-size=1280,800')

driver = webdriver.Chrome(options=options)
driver.get('https://example.com')
print(driver.title)
driver.quit()

JavaScript (Node.js)

const { Builder } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');

const options = new chrome.Options();
options.addArguments('--headless=new');
options.addArguments('--no-sandbox');
options.addArguments('--disable-dev-shm-usage');
options.addArguments('--window-size=1280,800');

const driver = await new Builder()
  .forBrowser('chrome')
  .setChromeOptions(options)
  .build();

await driver.get('https://example.com');
console.log(await driver.getTitle());
await driver.quit();

Java

ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
options.addArguments("--no-sandbox");
options.addArguments("--disable-dev-shm-usage");
options.addArguments("--window-size=1280,800");

WebDriver driver = new ChromeDriver(options);
driver.get("https://example.com");
System.out.println(driver.getTitle());
driver.quit();

Headless Chrome in Docker

Docker containers don't have displays, so headless Chrome is required. There are also sandbox restrictions you need to handle.

Dockerfile

FROM node:20-slim

# Install Chrome dependencies
RUN apt-get update && apt-get install -y \
  google-chrome-stable \
  --no-install-recommends \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
CMD ["npm", "test"]

Or use the official Playwright Docker image (includes Chrome, Firefox, WebKit):

FROM mcr.microsoft.com/playwright:v1.42.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]

Required Chrome Arguments in Docker

// Puppeteer in Docker
const browser = await puppeteer.launch({
  args: [
    '--no-sandbox',
    '--disable-setuid-sandbox',
    '--disable-dev-shm-usage',  // critical — Docker limits /dev/shm to 64MB by default
    '--disable-gpu',
  ]
});

The --disable-dev-shm-usage flag is critical in Docker. Chrome uses /dev/shm (shared memory) for rendering. Docker containers limit this to 64MB by default, which causes Chrome to crash on memory-intensive pages. This flag tells Chrome to use /tmp instead.

Increase /dev/shm in Docker Compose

Alternatively, increase shared memory:

# docker-compose.yml
services:
  tests:
    image: your-test-image
    shm_size: '2gb'

Or with docker run:

docker run --shm-size=2g your-test-image

Headless Chrome in CI/CD

GitHub Actions

name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci

      # Playwright — installs Chrome automatically
      - run: npx playwright install chromium --with-deps

      - run: npx playwright test

GitHub Actions Ubuntu runners have Chrome pre-installed, so Playwright finds it automatically. The --with-deps flag installs system dependencies needed for headless Chrome.

GitLab CI

test:
  image: mcr.microsoft.com/playwright:v1.42.0-jammy
  script:
    - npm ci
    - npx playwright test

CircleCI

version: 2.1
jobs:
  test:
    docker:
      - image: mcr.microsoft.com/playwright:v1.42.0-jammy
    steps:
      - checkout
      - run: npm ci
      - run: npx playwright test

Common Headless Chrome Issues

Chrome crashes immediately

Cause: Memory issue in Docker (/dev/shm too small)

Fix:

args: ['--disable-dev-shm-usage']

"No usable sandbox" error

Cause: Running Chrome as root or in a restricted container

Fix:

args: ['--no-sandbox', '--disable-setuid-sandbox']

Note: --no-sandbox reduces security isolation. Only use in trusted test environments.

Elements not found / different layout

Cause: Viewport not set, defaulting to 800x600

Fix:

await page.setViewportSize({ width: 1280, height: 720 });

Screenshots are blank or wrong size

Cause: Page not fully loaded, or viewport not set

Fix:

await page.goto(url, { waitUntil: 'networkidle' });
await page.setViewportSize({ width: 1280, height: 720 });
await page.screenshot({ path: 'screenshot.png', fullPage: true });

Fonts render differently

Cause: System fonts differ between CI and local

Fix: Use the Playwright Docker image, which has consistent fonts. Or install specific fonts in your Dockerfile.

Tests pass locally, fail in CI

Cause: Often timing issues amplified by headless + slower CI CPU

Fix:

  • Increase timeouts: timeout: 30000
  • Use waitForLoadState('networkidle') for slow pages
  • Check for race conditions with async operations

Headless Chrome vs Headless Firefox vs WebKit

Playwright supports all three browser engines in headless mode.

import { chromium, firefox, webkit } from 'playwright';

// Chrome/Chromium (most common)
const browser = await chromium.launch();

// Firefox
const browser = await firefox.launch();

// Safari engine (WebKit)
const browser = await webkit.launch();

In playwright.config.ts, run tests across all three:

export default defineConfig({
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
  ],
});

When to Use Headed Mode

Despite headless being the default for CI, headed mode is essential for:

Debugging failing tests: See exactly what's happening in the browser. Add await page.pause() to stop at a specific point and inspect the DOM.

Recording tests: Playwright Codegen (npx playwright codegen) runs in headed mode.

Visual verification: Confirming that animations, hover states, and viewport-dependent layouts look correct.

Developing new tests: Writing tests is faster when you can see the browser respond to your commands.

Headless Chrome for Screenshots and PDFs

Headless Chrome is commonly used outside of testing — for generating screenshots and PDFs of web pages.

Screenshot with Playwright

const browser = await chromium.launch();
const page = await browser.newPage();
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto('https://example.com', { waitUntil: 'networkidle' });

// Full page screenshot
await page.screenshot({ path: 'full-page.png', fullPage: true });

// Specific element
await page.locator('.report-card').screenshot({ path: 'card.png' });

await browser.close();

PDF with Playwright

await page.goto('https://example.com');
await page.pdf({
  path: 'report.pdf',
  format: 'A4',
  printBackground: true,
});

Summary

Headless Chrome is Chrome without a visible window. Use it in:

  • CI/CD pipelines — servers have no display
  • Parallel test runs — no display conflicts
  • Screenshot/PDF generation — server-side rendering
  • Web scraping — JavaScript-rendered content

Key configuration points:

  • Playwright uses headless by default — no setup needed
  • In Docker, always add --no-sandbox and --disable-dev-shm-usage
  • Use --headless=new flag with Selenium (Chrome 112+)
  • Set explicit viewport size to avoid layout issues

For debugging, switch to headed mode with headless: false or --headed flag.


Running Playwright tests in CI? HelpMeTest runs your browser tests continuously — headless Chrome in the cloud, with self-healing when selectors break.

Read more