Skip to content

Selenium headless mode: run Chrome headless, and what sites can detect

We tested headless Chrome 143 with Selenium 4.51: --headless=old fails, options.headless does nothing, and the user agent says HeadlessChrome. The fixes.

HProxy TeamOctober 12, 2026Updated October 12, 202610 min read

Selenium headless mode: run Chrome headless, and what sites can detect

To run Chrome headless in Selenium with Python, add --headless=new to ChromeOptions and pass the options to webdriver.Chrome(). Since Chrome 132, plain --headless starts the same mode, and --headless=old fails. The old engine lives on as a separate program, chrome-headless-shell. Give the window a size as well. A headless Chrome still tells sites it is headless in its user agent, and Selenium sets navigator.webdriver to true in every mode.

We tested this on our own server on 12 October 2026, with Selenium 4.51.0, Python 3.12.3 and Chrome for Testing 143. A test page of our own recorded what each setup sent, and a script on the page read what it could. Every example below ran there, and its output is pasted under it.

What you need

  • Python 3.10 or newer. Selenium 4.51.0 came out on 9 October 2026 and needs it.
  • Chrome, and Selenium itself: pip install selenium. Selenium fetches the matching driver for you.
  • On Linux as the root user, the --no-sandbox flag as well. Started as root without it, Chrome quit at once. The runs behind this page also passed --no-sandbox and --disable-dev-shm-usage, outside the examples.

Step 1: start Chrome headless

from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)

driver.get("https://hproxy.com/blog/web-scraping-with-python")
print(driver.title)
print(driver.get_window_size())
driver.save_screenshot("page.png")
Web Scraping With Python: Step by Step, Tested | HProxy
{'width': 1920, 'height': 1080}

--headless=new starts Chrome without a window. --window-size matters as much: without it, the window was 780 by 580 pixels, as the section on the window below shows. save_screenshot() works headless too and saves what the page shows.

Step 2: check what the page sees

Every page can read a few facts about the browser with one line of JavaScript each. Check them in your own setup.

seen = driver.execute_script("""
return {
  userAgent: navigator.userAgent,
  webdriver: navigator.webdriver,
  window: outerWidth + "x" + outerHeight,
  screen: screen.width + "x" + screen.height,
};
""")
for name, value in seen.items():
    print(f"{name:10} {value}")
screen     800x600
userAgent  Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/143.0.0.0 Safari/537.36
webdriver  True
window     1920x1080

Three things stand out. The user agent says HeadlessChrome. navigator.webdriver is true. And the window is larger than the screen, which stayed at 800 by 600. The sections below show where each one comes from and which flag changes it.

--headless, --headless=new, --headless=old or the shell?

Chrome had two headless modes. Its documentation explains that the old one "was a separate, alternate browser implementation". Since Chrome 132, the old mode "is only available as a standalone binary named chrome-headless-shell". Chrome's announcement put it plainly: "Running Chrome with --headless or --headless=new runs the new Headless mode".

We started each option through Selenium and recorded what a page got.

SetupStarts?User agentPluginsWindow
--headless=newyesHeadlessChrome/143.0.0.05780 x 580
--headlessyesHeadlessChrome/143.0.0.05780 x 580
--headless=oldno: the browser exits
the headless shellyesHeadlessChrome/143.0.7499.1920800 x 600
no headless flag, no displayno: the browser exits
Terminal output of headless_tests.py: the browser's own message for --headless=old and for a start without a display, then one row per setup with its user agent, client hint brand, navigator.webdriver, plugins, window, screenshot and screen size, then the headless shell against new headless over 10 starts
Our own capture, 12 October 2026: headless_tests.py on our server, Selenium 4.51.0 with Chrome for Testing 143

--headless and --headless=new gave the same result in every column we read. With --headless=old, the browser printed "Old Headless mode has been removed from the Chrome binary" and exited. So use --headless=new: it means the same today and states your choice.

The headless shell is the old engine as its own program. It is easy to tell apart. It sent the full version, 143.0.7499.192, and a "HeadlessChrome" brand in its client hints. It also had no plugins, no window.chrome object, and notifications set to "denied". New headless gave "default" there.

Is headless faster?

We could not compare headless Chrome with a visible window: our server has no display. We could compare the two headless builds. Chrome's documentation says the shell has "substantially fewer dependencies" and is "in some ways more performant". We started each one 10 times, in turn, and loaded the same page.

Median of 10 startsNew headlessHeadless shell
Start0.93 s0.36 s
Page load0.309 s0.197 s
Screenshot0.157 s0.082 s
Private memory135 MB75 MB
Processes86
Private memory after one page
new headless
135 MB
headless shell
75 MB
Source: HProxy test on our server, 12 October 2026: Chrome for Testing and chrome-headless-shell 143; middle of 10 starts · hproxy.comProxy.

The shell started more than twice as fast and needed a little over half the memory. The page loads overlapped too much to call: new headless took 0.106 to 0.683 s, and the shell 0.142 to 1.185 s. Chrome's documentation makes the same split. It calls the shell "suitable for use cases such as automated screenshotting or web scraping", and new headless "the real Chrome browser".

Why options.headless = True does nothing

Older tutorials set options.headless = True. Selenium deprecated that property in version 4.8.0 and removed it from the Python bindings in 4.13.0. Today the line still runs.

old = webdriver.ChromeOptions()
old.headless = True
print(old.arguments)
[]

No error, and no headless argument either. Chrome then starts with a window. On a server without a display, that start fails with "session not created: Chrome instance exited". Selenium's blog explains the change: headless "is not actually going away, just the convenience method to set it in Selenium". Add --headless=new as an argument instead.

The headless window is small

New headless opened a window of 780 by 580 pixels. The screenshot, which is what the page shows, was 780 by 388. A page can change its layout at a width like that. Our test page hid its menu below 1,024 pixels.

Two screenshots of one test page in headless Chrome 143: at the default window the menu is a Menu button; at 1920 by 1080 the same corner shows the links Home, Deals and Contact
Our own screenshots, 12 October 2026: Selenium 4.51.0 with Chrome for Testing 143 on our server

At the default size, the Deals link was not displayed: Selenium's is_displayed() returned false for it. Our page on finding elements in Selenium shows what that does to a search. These setups gave these sizes.

SetupWindowVisible pageScreen
--headless=new780 x 580780 x 388800 x 600
plus --start-maximized800 x 600800 x 408800 x 600
plus --window-size=1920,10801920 x 10801920 x 888800 x 600
plus --screen-info={1920x1080}1920 x 10801920 x 8881920 x 1080

--start-maximized fills an 800 by 600 screen, so it is no fix. --window-size is. The screen stays at 800 by 600 because headless Chrome draws on a virtual screen. Chrome's documentation says its size "can be specified using the --screen-info command line switch".

driver.quit()
options.add_argument("--screen-info={1920x1080}")
driver = webdriver.Chrome(options=options)
driver.get("https://hproxy.com/blog/web-scraping-with-python")
print(driver.execute_script("return [screen.width, screen.height, outerWidth, outerHeight]"))
driver.quit()
[1920, 1080, 1920, 1080]

What a site can see, and what each flag changes

These are the signals our test page read. Each row shows new headless as it starts, then the flag that changed the signal in our test.

SignalNew headlessWhat changed it
User-Agent header and navigator.userAgentHeadlessChrome/143.0.0.0--user-agent, or the user agent override below
Sec-CH-UA header (client hints)"Chromium";v="143", "Not A(Brand";v="24"nothing needed, but an override can remove it
navigator.webdrivertrue--disable-blink-features=AutomationControlled
Screen800 x 600--screen-info
Window780 x 580--window-size

The user agent is the plainest signal. You can change it on a running browser with a DevTools command. The catch is the client hints. A normal Chrome sends the Sec-CH-UA header with its brands by default, in MDN's words "without the server opting in". Override the user agent alone, and that header disappears.

hints = driver.execute_script("return {brands: navigator.userAgentData.brands, platform: navigator.userAgentData.platform}")
ua = driver.execute_script("return navigator.userAgent").replace("HeadlessChrome", "Chrome")

driver.execute_cdp_cmd("Network.setUserAgentOverride", {"userAgent": ua})
driver.get("https://hproxy.com/blog/web-scraping-with-python")
print(driver.execute_script("return [navigator.userAgent, navigator.userAgentData.brands.length]"))
['Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36', 0]

The user agent now says Chrome, and the browser reports 0 brands. Our test server saw no Sec-CH-UA header at all for this setup. The DevTools Protocol states the rule: "userAgentMetadata must be set for Client Hint headers to be sent". So pass the browser's own brands along, read before the override.

driver.execute_cdp_cmd("Network.setUserAgentOverride", {
    "userAgent": ua,
    "userAgentMetadata": {"brands": hints["brands"], "platform": hints["platform"], "platformVersion": "",
                          "architecture": "", "model": "", "mobile": False},
})
driver.get("https://hproxy.com/blog/web-scraping-with-python")
print(driver.execute_script("return navigator.userAgentData.brands.map(b => b.brand + ' ' + b.version)"))
['Chromium 143', 'Not A(Brand 24']

With the metadata, our test server got the Sec-CH-UA header back. Read navigator.userAgentData after opening an https page: MDN marks it "available only in secure contexts (HTTPS)". The --user-agent flag at start changed both user agents in our test and kept the client hints. It needs the full string written out, so it goes stale when Chrome updates.

navigator.webdriver is a different story. It was true in every setup Selenium started, new headless and the shell alike. The WebDriver standard sets it whenever "the user agent is under remote control". Only --disable-blink-features=AutomationControlled turned it false in our test, and the user agent still said HeadlessChrome.

None of these flags changes the address a site sees. If a site still refuses the browser after these checks, the address may be the reason. Our guide to proxies in Selenium shows how to set one, and our residential proxies are the paid option. Our BrowserScan page explains what a full fingerprint test looks at.

Errors that stop a headless start

Each of these setups failed in our tests, and Selenium reported the same line for all of them: "session not created: Chrome instance exited".

CauseWhat the browser itself saidFix
No headless flag, no displayMissing X server or $DISPLAYAdd --headless=new
options.headless = Truethe same, since no flag is sentAdd --headless=new
--headless=oldOld Headless mode has been removed from the Chrome binaryUse --headless=new or the shell
Run as root without --no-sandbox(Chrome exited at once)Add --no-sandbox

Older Stack Overflow questions quote such failures as "DevToolsActivePort file doesn't exist", which chromedriver 143 did not print in our tests. When the message says nothing useful, start the browser by hand with the same flags. Started that way with --headless=old or without a display, Chrome's first error line named the cause in our tests.

What this page could not check

  • No visible window: our server has no display, so we could not compare headless with a normal window.
  • One browser build: Chrome for Testing 143, stable since 2 December 2025. Its client hints say Chromium, and Google Chrome's own build may differ. The stable Chrome on 11 October 2026 was 155, and the default window may differ there.
  • Firefox and Edge were not tested.
  • Times and memory come from one busy server and moved between runs. Over six runs, new headless started in a median 0.71 to 1.16 s and the shell in 0.27 to 0.59 s.
  • We read the signals listed above, not a full browser fingerprint.
  • We will rerun these tests by 12 January 2027.

Sources

Frequently asked questions

How do I run Chrome headless with Selenium in Python?

Add the --headless=new argument to ChromeOptions and pass the options to webdriver.Chrome. Add --window-size=1920,1080 as well, because the default headless window was only 780 by 580 pixels in our test of Chrome 143.

What is the difference between --headless and --headless=new?

Since Chrome 132 there is none: both start the new headless mode, which is the normal Chrome without a window. In our test of Chrome 143 they gave the same user agent, plugins and window size. --headless=old now makes Chrome exit with an error.

Is Selenium headless faster?

We could not compare it with a visible window, because our server has no display. Between the two headless builds, the old headless shell started in 0.36 seconds against 0.93 and held 75 MB of private memory against 135 MB in our test.

Why can Selenium not find an element in headless mode?

A small headless window can hide it. In our test the page got only 780 by 388 pixels, and a page that hides its menu below 1,024 pixels hid it. Start Chrome with --window-size=1920,1080 or call driver.set_window_size(1920, 1080).

How do I change the user agent of headless Chrome?

Pass --user-agent with a normal Chrome string, or call the DevTools command Network.setUserAgentOverride. If you use the command, also pass userAgentMetadata with the browser's own brands. Without it, Chrome sent no Sec-CH-UA header at all in our test, a header Chrome sends by default.

Why does headless Chrome not start on a Linux server?

In our tests Selenium reported session not created: Chrome instance exited for three reasons: no headless flag on a server without a display, the removed --headless=old flag, and Chrome run as root without --no-sandbox. Older Stack Overflow questions quote the same failure as DevToolsActivePort file doesn't exist.

Can Selenium take screenshots in headless mode?

Yes. driver.save_screenshot() works headless, and the image has the size of the visible page: 780 by 388 pixels at the default window in our test, and 1920 by 888 with --window-size=1920,1080.

How do I run headless Firefox with Selenium?

Selenium's documentation passes the -headless argument to FirefoxOptions. We did not test Firefox for this page.

Get proxies that are alive right now

Our free proxy list re-checks every exit every few minutes across 100+ countries, with a live last-checked time, so you copy IPs that worked moments ago, not a stale text dump. When the location has to survive a real check, the paid network holds up.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup

HProxy.

Honest guides and comparisons on proxies, scraping and staying unblocked, from the team that runs the network.

RSS feed