Why Selenium WebDriver Screenshots Differ in Headless Mode and How to Fix Them
Learn why Selenium screenshots differ in headless runs and how to control viewport, scale, versions, capture scope, and page timing.
Selenium screenshots can differ in headless mode because the browser did not render under exactly the same conditions, or because the two captures covered different areas of the page. Compare the effective window and CSS viewport, browser and driver versions, headless implementation, screen scale, screenshot scope, operating system, and page readiness before comparing pixels.
Headless mode is not automatically a source of different pixels. Current Chrome Headless and headful modes share the browser implementation, but their screen configuration, timing, binary version, and capture settings can still differ. Treat every screenshot as the output of a measured rendering environment.
What to check first
- Record the exact browser version, driver version, operating system or container image, and headless flags.
- Set a known outer window size, then read back the resulting window rectangle.
- Measure
window.innerWidth,window.innerHeight,devicePixelRatio, andvisualViewportinside the page. - Confirm whether both captures are viewport screenshots or full-document screenshots.
- Wait for the same application state, fonts, images, and layout work before capturing.
- Save the measured values beside each image. Diagnose environment differences before inspecting individual pixels.
Why headless screenshots change
Window size is not the CSS viewport
Selenium controls a browser window, while responsive CSS uses the page viewport. Browser chrome, platform behavior, and driver implementation can make the requested outer dimensions different from innerWidth and innerHeight. Selenium documents window-management methods such as set_window_size, and notes that screen resolution can affect how an application renders. Set the size and then inspect what the page actually received.
Chrome’s command-line documentation shows an explicit headless screenshot size, for example --window-size=412,892. That value is a configuration input, not proof that the page’s CSS viewport has those dimensions. Selenium window and tab documentation and the Chrome Headless command-line reference describe these controls.
Screen scale changes raster output
A page can have the same CSS dimensions but a different device pixel ratio or screen scale. Text and borders then land on different physical pixels. Measure devicePixelRatio and, when using Chrome Headless screen configuration, control the virtual screen size and scale factor. Chrome states that Headless screens are independent of physically attached displays; its virtual-screen guide documents the available size and scale configuration.
Browser, driver, and Headless implementation versions matter
Record exact versions in every reproducibility report. Chrome 112 updated Headless to create platform windows without displaying them and describes current Headless and headful modes as unified. From Chrome 132.0.6793.0, the old Headless implementation is available as the separate chrome-headless-shell binary. A run using that binary is a different implementation from a run using current Chrome Headless. See Chrome Headless mode.
The screenshot API may capture a different scope
A normal Chromium WebDriver screenshot generally captures the current window or viewport. A full-document image is a separate operation, and browser APIs do not all expose it in the same way. Firefox, for example, provides a specifically named full-page screenshot API. If one test captures the viewport and another captures the document, their heights and visible sections will differ even when the page is identical. The Selenium Chromium WebDriver API reference documents the Chromium screenshot behavior.
Page state may not be ready
Animations, delayed data, lazy images, web fonts, consent banners, and client-side layout changes can produce different pixels. Wait for the application state your test requires, then capture. Use a deterministic test fixture where possible and avoid relying on an arbitrary sleep as the only readiness signal.
A reproducible Selenium diagnostic in Python
Install Selenium with python -m pip install selenium. Selenium Manager can obtain a compatible driver in current Selenium releases; pin the browser and driver in CI when repeatability matters.
from pathlib import Path
import json
import platform
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
URL = 'https://example.com'
OUT = Path('diagnostic.png')
options = Options()
options.add_argument('--headless=new')
options.add_argument('--window-size=1280,800')
# Add an explicit scale when your CI image requires it:
# options.add_argument('--force-device-scale-factor=1')
driver = webdriver.Chrome(options=options)
try:
driver.get(URL)
driver.set_window_size(1280, 800)
# Replace this predicate with an application-specific readiness check.
WebDriverWait(driver, 30).until(
lambda d: d.execute_script('return document.readyState') == 'complete'
)
time.sleep(0.25) # Allow a known, documented settling period if needed.
rect = driver.get_window_rect()
viewport = driver.execute_script('''
const vv = window.visualViewport;
return {
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
clientWidth: document.documentElement.clientWidth,
clientHeight: document.documentElement.clientHeight,
devicePixelRatio: window.devicePixelRatio,
visualViewport: vv ? {
width: vv.width, height: vv.height,
scale: vv.scale, offsetLeft: vv.offsetLeft, offsetTop: vv.offsetTop
} : null
};
''')
report = {
'browser': driver.capabilities.get('browserName'),
'browserVersion': driver.capabilities.get('browserVersion'),
'driverVersion': driver.capabilities.get('chrome', {}).get('chromedriverVersion'),
'platform': platform.platform(),
'windowRect': rect,
'viewport': viewport,
'headlessArguments': ['--headless=new', '--window-size=1280,800'],
}
print(json.dumps(report, indent=2))
Path('diagnostic.json').write_text(json.dumps(report, indent=2))
# This is a current-window screenshot, not automatically a full-document image.
driver.save_screenshot(str(OUT))
finally:
driver.quit()
Run the same script in both environments and compare diagnostic.json before comparing diagnostic.png. If the CSS viewport differs, fix window or screen configuration first. If the measurements match, investigate capture scope and page readiness.
Controlling Chrome Headless dimensions
Use one source of truth for dimensions. You can set them through Selenium’s window API, Chrome startup options, or both, then log the observed values. Chrome’s documented command-line pattern is:
chrome --headless --screenshot --window-size=412,892 https://developer.chrome.com/
For a controlled virtual screen, consult Chrome’s virtual-screen configuration guide. Its 800 by 600 and 600 by 800 screen examples illustrate configuration only; they are not universal recommended sizes.
Viewport versus full-page capture
| Question | Viewport capture | Full-page capture |
|---|---|---|
| What is included? | The currently visible window area. | The document’s scrollable content, when the chosen API supports it. |
| Why can height differ? | Viewport height and device scale determine the image. | Document layout, lazy loading, sticky elements, and browser-specific full-page behavior affect the result. |
| What should be compared? | CSS viewport and device pixel ratio. | Those values plus the full-page method and document height. |
Choose the scope deliberately and use the same API and browser for both runs. Do not compare a viewport screenshot with a stitched or full-document image.
Make page readiness deterministic
- Wait for a specific application element or state, not only
document.readyState. - Ensure images needed for the comparison have loaded and have stable dimensions.
- Disable or await animations and transitions in the test fixture.
- Use the same test data, locale, timezone, authentication state, and URL parameters.
- Capture after fonts and client-side layout calculations have settled.
- Keep a diagnostic screenshot and environment report for failed comparisons.
These are test-design practices. They improve repeatability but do not guarantee pixel identity across every operating system, font installation, compositor, or dynamic page.
Troubleshooting common mismatches
| Symptom | Likely cause | Fix |
|---|---|---|
| Everything is wider or narrower | Different CSS viewport or browser window. | Set the same window size, read back the rectangle, and compare innerWidth and innerHeight. |
| Text or one-pixel borders differ | Different device pixel ratio or screen scale. | Measure devicePixelRatio; control the Headless virtual screen and scale factor. |
| One image is much taller | Viewport versus full-page scope, or different document height. | Use the same screenshot API and verify the intended scope. |
| Only lower sections differ | Lazy content, delayed data, or an unsettled layout. | Wait for the relevant content and image loads before capture. |
| Results changed after a browser upgrade | Browser, driver, or Headless implementation changed. | Record exact versions and pin them while diagnosing. |
| Headless and headed runs disagree | Different screen environment or startup flags. | Compare measured viewport, scale, flags, operating system, and timing; do not assume Headless alone is responsible. |
| Screenshot call succeeds but output is unexpected | The method captures the current window rather than the full document. | Use a documented full-page API when that is the requirement. |
Performance, reliability, and cost considerations
- Fixed viewports make visual diffs cheaper to diagnose because a mismatch has fewer variables.
- Reusing a browser session can reduce startup time, but reset cookies, storage, page state, and test data between cases.
- Waiting for network idle or images improves fidelity but increases latency; prefer an application-specific readiness condition.
- Full-page captures and high device scale factors produce larger images and may consume more memory.
- Keep browser and driver versions aligned. A reproducible container image is easier to audit than an implicitly updated host browser.
- Selenium itself has no screenshot billing. Your costs come from the machines, CI minutes, storage, and any external capture service you add.
Or skip the browser setup
ScreenshotNeo provides a single-request website screenshot API and an MCP server. It accepts a URL and returns PNG, JPEG, WebP, or PDF. Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Responses identify the page verdict and billing status with X-Page-Verdict and X-Billed headers. An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
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)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
const buffer = Buffer.from(await res.arrayBuffer());
await require('node:fs').promises.writeFile('shot.webp', buffer);
Options include full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets or custom viewports, retina scale, custom CSS and JavaScript, click and hide selectors, selector or delay waits, network-idle waits, request blocking, headers, cookies, user agents, Authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify migration.
The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is on every plan. Create a free ScreenshotNeo account.
FAQ
Does Headless mode always produce different screenshots?
No. Differences depend on the effective environment, browser implementation, capture scope, and page state. Current Chrome Headless and headful modes share the browser implementation, but matching pixels still requires matching configuration and timing.
Should I trust --window-size as the viewport?
Use it as an input, then measure the resulting window and CSS viewport. Outer-window dimensions are not automatically CSS viewport dimensions.
What versions should a bug report include?
Include browser and driver versions, operating system or container, Headless implementation and flags, requested and observed window sizes, CSS viewport and scale values, screenshot scope, and page readiness details.
How can I tell whether the image is full page?
Check the specific WebDriver method and its browser documentation. A normal Chromium screenshot is a current-window capture; full-page support is a distinct API or technique.
Is there a pixel-identical configuration for every machine?
No configuration can be treated as a universal guarantee. Font availability, operating-system rendering, compositor behavior, dynamic content, and timing may still require investigation.


