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-sandboxand--disable-dev-shm-usage - Use
--headless=newflag 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.