Selenium Headless: Run Chrome and Firefox Without a Display
Headless mode runs a browser without displaying a graphical interface. In Selenium, enable it by adding the --headless argument to Chrome or Firefox options before creating the driver. Headless is required for CI/CD servers (no display), Docker containers, and background test runs. Tests behave identically to headed mode — same rendering engine, same JavaScript execution, same network requests.
Key Takeaways
Headless mode is not a different browser. It's the same Chrome or Firefox, running without rendering pixels to a screen. Selenium automation works identically — same API, same selectors, same behavior.
Always set a window size in headless mode. Without a display, browsers default to a small viewport (800x600 or smaller). This causes layout differences that don't exist in production. Set --window-size=1920,1080 explicitly.
New headless is different from old headless in Chrome. Chrome 112+ introduced --headless=new which uses the same rendering pipeline as headed Chrome. The old --headless (now --headless=old) had subtle differences. Use --headless=new for consistency.
Headless Chrome has limitations. GPU-accelerated rendering, clipboard operations, some browser extensions, and getUserMedia (camera/microphone) don't work in headless mode. If your tests exercise these features, you need a display (use Xvfb on Linux) or a cloud browser service.
For CI/CD, headless is the standard. GitHub Actions, CircleCI, Jenkins, GitLab CI all support headless Chrome natively without additional configuration. If your CI runner is Linux, headless is the correct choice.
What Is Headless Mode?
A headed browser opens a visible window on your screen. You see the browser, the pages loading, the interactions happening.
A headless browser does all the same work — parsing HTML, executing JavaScript, rendering the DOM, handling network requests — but without displaying anything. No window, no GUI, no visual output.
For automated testing, this means:
- Tests run on servers without displays (CI/CD, Docker, remote servers)
- Tests run faster (no time spent rendering pixels to a screen)
- Tests run in parallel without interfering with each other
- No pop-ups or browser windows disrupt your workflow
Headless mode is not a separate product. It's the same Chrome or Firefox with a flag that disables the display layer.
Setting Up Headless Chrome in Selenium (Python)
Basic Setup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
chrome_options = Options()
chrome_options.add_argument("--headless=new") # Headless mode
chrome_options.add_argument("--window-size=1920,1080") # Set viewport size
chrome_options.add_argument("--no-sandbox") # Required in Docker/CI
chrome_options.add_argument("--disable-dev-shm-usage") # Prevent memory issues in Docker
driver = webdriver.Chrome(options=chrome_options)
driver.get("https://example.com")
print(driver.title) # Works identically to headed mode
driver.quit()
Old vs New Headless Chrome
Chrome introduced a new headless implementation in version 112:
# Old headless (Chrome < 112, still supported but deprecated)
chrome_options.add_argument("--headless")
# or explicitly
chrome_options.add_argument("--headless=old")
# New headless (Chrome 112+, recommended)
chrome_options.add_argument("--headless=new")
The new headless (--headless=new) uses the same rendering pipeline as headed Chrome. The old headless used a separate path that caused subtle differences in rendering and JavaScript behavior. Use --headless=new for any Chrome 112 or later.
Recommended Options for CI/CD
chrome_options = Options()
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--window-size=1920,1080")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-gpu") # Recommended for headless
chrome_options.add_argument("--disable-extensions") # No extensions in CI
chrome_options.add_argument("--disable-logging") # Reduce log noise
chrome_options.add_argument("--log-level=3") # Only fatal errors
Setting Up Headless Firefox in Selenium (Python)
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
firefox_options = Options()
firefox_options.add_argument("--headless")
firefox_options.add_argument("--width=1920")
firefox_options.add_argument("--height=1080")
driver = webdriver.Firefox(options=firefox_options)
driver.get("https://example.com")
print(driver.title)
driver.quit()
Firefox headless does not have an "old" vs "new" distinction. The --headless flag has been stable since Firefox 56.
Headless Setup in JavaScript (Node.js / selenium-webdriver)
const { Builder } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
async function runHeadlessTest() {
const options = new chrome.Options();
options.addArguments('--headless=new');
options.addArguments('--window-size=1920,1080');
options.addArguments('--no-sandbox');
options.addArguments('--disable-dev-shm-usage');
const driver = await new Builder()
.forBrowser('chrome')
.setChromeOptions(options)
.build();
try {
await driver.get('https://example.com');
const title = await driver.getTitle();
console.log('Title:', title);
} finally {
await driver.quit();
}
}
runHeadlessTest();
Running in Docker
Docker containers don't have a display. Headless mode is required.
Dockerfile:
FROM python:3.11-slim
# Install Chrome
RUN apt-get update && apt-get install -y \
wget \
gnupg \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list \
&& apt-get update \
&& apt-get install -y google-chrome-stable \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
RUN pip install selenium webdriver-manager
COPY test_script.py .
CMD ["python", "test_script.py"]
test_script.py:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
options = Options()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()
The --no-sandbox and --disable-dev-shm-usage flags are critical in Docker. Without them, Chrome crashes on resource-restricted containers.
Running in GitHub Actions
GitHub Actions runners have Chrome pre-installed. Headless just works:
name: Selenium Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: pip install selenium webdriver-manager
- name: Run Selenium tests
run: python -m pytest tests/
Your test code uses headless Chrome options as shown above. No additional setup is needed — GitHub Actions Ubuntu runners have Chrome and a compatible ChromeDriver installed.
Taking Screenshots in Headless Mode
Screenshots work identically in headless mode:
driver = webdriver.Chrome(options=chrome_options)
driver.get("https://example.com")
# Full page screenshot (current viewport only)
driver.save_screenshot("screenshot.png")
# Screenshot a specific element
element = driver.find_element(By.ID, "main-content")
element.screenshot("element_screenshot.png")
Screenshots are especially useful in headless CI pipelines — when a test fails, save a screenshot to understand what the page looked like at the time of failure:
import pytest
@pytest.fixture(autouse=True)
def save_screenshot_on_failure(driver, request):
yield
if request.node.rep_call.failed:
driver.save_screenshot(f"failure_{request.node.name}.png")
Common Issues in Headless Mode
Window Size / Viewport Issues
Without a display, browsers default to a small viewport. Elements positioned for full-screen layouts may not be visible or interactable.
# Always set an explicit window size
chrome_options.add_argument("--window-size=1920,1080")
# Or set after driver creation
driver.set_window_size(1920, 1080)
If tests pass in headed mode but fail headless, viewport size is the first thing to check.
Element Not Interactable in Headless
Some elements become non-interactable in headless mode because they're outside the viewport or rendered differently.
# Scroll element into view before clicking
element = driver.find_element(By.ID, "submit-button")
driver.execute_script("arguments[0].scrollIntoView(true);", element)
element.click()
PDF Downloads
Chrome in headless mode can generate PDF output but doesn't save file downloads the same way as headed mode. For download testing:
# Set download directory
prefs = {
"download.default_directory": "/tmp/downloads",
"download.prompt_for_download": False,
"download.directory_upgrade": True,
}
chrome_options.add_experimental_option("prefs", prefs)
# Allow downloads in headless mode (Chrome 73+)
driver.execute_cdp_cmd(
"Page.setDownloadBehavior",
{"behavior": "allow", "downloadPath": "/tmp/downloads"}
)
JavaScript-Heavy SPAs
In headless mode, JavaScript execution timing can differ slightly. If your SPA relies on requestAnimationFrame or other animation-linked APIs:
# Wait for JavaScript initialization to complete
WebDriverWait(driver, 10).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
# Wait for SPA framework to finish rendering
WebDriverWait(driver, 10).until(
lambda d: d.execute_script("return typeof angular === 'undefined' || angular.element(document.body).injector().get('$http').pendingRequests.length === 0")
)
Headless Chrome vs Selenium Grid vs Cloud Browsers
| Approach | Use Case | Cost | Setup |
|---|---|---|---|
| Local headless Chrome | Development, single machine CI | Free | Minimal |
| Docker + headless Chrome | Containerized CI/CD | Free + infra | Medium |
| Selenium Grid | Parallel tests, multiple browsers | Free + infra | High |
| BrowserStack / Sauce Labs | Cross-browser, real devices | $29-$200+/mo | Low |
| HelpMeTest | AI-powered testing, no infra | $100/mo flat | Minimal |
For most teams, headless Chrome in CI covers 80% of needs. For cross-browser testing, real mobile devices, or teams without DevOps resources, cloud browser services eliminate infrastructure management.
Checking If Running in Headless Mode
Useful for writing tests that adapt behavior based on environment:
def is_headless(driver):
"""Check if the browser is running in headless mode."""
return "--headless" in driver.execute_script(
"return navigator.userAgent"
) or driver.execute_script(
"return window.chrome === undefined"
)
Note: this is an imperfect detection method. Chrome's headless mode leaks into navigator.userAgent in some versions but not others. Prefer passing an explicit HEADLESS=true environment variable in your test setup.
Complete Working Example
import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def create_driver(headless=None):
"""Create a Chrome driver, headless if environment demands it."""
if headless is None:
headless = os.getenv("CI", "false").lower() == "true"
options = Options()
if headless:
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")
options.add_argument("--window-size=1920,1080")
return webdriver.Chrome(options=options)
def test_homepage():
driver = create_driver()
try:
driver.get("https://example.com")
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.TAG_NAME, "h1"))
)
assert "Example" in driver.title
print(f"Test passed. Title: {driver.title}")
except Exception as e:
driver.save_screenshot("failure.png")
raise e
finally:
driver.quit()
test_homepage()
Run locally with UI: python test.py Run in CI (headless): CI=true python test.py
Summary
Headless Selenium is the standard for CI/CD and containerized test environments. The setup is minimal — add --headless=new to Chrome options or --headless to Firefox options, set a window size, and your existing tests run unchanged.
Key points:
- Use
--headless=newfor Chrome 112+ - Always set
--window-size=1920,1080 - Add
--no-sandboxand--disable-dev-shm-usagefor Docker - Take screenshots on failure for debugging
- Tests behave the same as headed mode — same API, same selectors