Chromium Headless Mode Differences Between Old and New Headless

Chromium's headless mode got torn apart and rebuilt between 2017 and 2023, and the two versions differ enough that treating them as interchangeable will break your test suite in ways that take a full afternoon to trace back to their source. The short version: old headless was a separate, stripped-down browser that happened to share a rendering engine with Chrome, while new headless is Chrome itself with the window turned off. Keep that distinction in your pocket, because it explains basically everything else in this piece.
The architectural split that caused every practical difference downstream
Chrome shipped its original headless mode in 2017, and the engineering call behind it made sense at the time: build a lightweight wrapper around Chromium's //content module, skip the //chrome layer entirely, and ship something fast and dependency-free for automation. That //chrome layer is where all the browser-level stuff lives, window management, extensions, the UI chrome you never think about until it's gone. Old headless lacked all of that code, trimmed down to a bare rendering core.
New headless showed up in Chrome 96 in late 2021, tucked behind the flag --headless=chrome, and it took the opposite approach. It's the entire Chrome browser, every code path intact, with platform window creation switched off. No pixels hit a screen, but everything else runs exactly like it does on the browser sitting on your desktop right now.
So old headless was really a different browser wearing Blink as a mask, built around that trimmed //content module, while new headless carries the full //chrome layer, just with the window suppressed. Once you've got that straight, the rest of this piece stops reading like trivia and starts reading like a chain of consequences you could've predicted yourself: performance differences fall out of codebase size, fingerprint differences fall out of which code paths exist, system dependencies follow the same logic. The timeline backs this up too. The flag became --headless=new in Chrome 109, went fully official in Chrome 112 in early 2023, took over as the default behavior of the plain --headless flag in Chrome 128, and got ripped out of the Chrome binary entirely in Chrome 132. It survives now only as a standalone binary called chrome-headless-shell, distributed through the Chrome for Testing dashboard. If your CI scripts still assume the old flag works, that flag is gone, and the error message will not let you down easy.
What the old mode could not do that the new mode can
Old headless couldn't load browser extensions, full stop, with no caveats and no limited testing capacity. The extension code path lived in //chrome, and old headless never shipped //chrome, so if your test suite needed to check how your product behaves with an ad blocker or a password manager running, old headless had nothing for you.
PDFs make the gap easy to see. Point old headless at a PDF and it triggers a download, the way a bare file server would. Point new headless at the same PDF and it renders inline, the way an actual Chrome user sees it. That's not a cosmetic quirk. If you were testing a workflow that depends on in-browser PDF viewing, old headless had been quietly lying to you the whole time.
New headless treats extensions, cookies, and preferences as first-class citizens, the same surface area a headed session gets. Anything sitting behind a permission prompt or browser-native UI was either flaky or flat-out untestable under the old mode. And here's the part that trips people up mid-migration: screenshot output isn't equivalent between the two modes. The Playwright team says this plainly, and it's worth repeating because skipping it is exactly how teams end up staring at a wall of failed tests wondering what broke. Regenerate your baselines. There's no pixel parity waiting for you on the other side.
Performance characteristics of each mode and what drives them
Old headless is lighter because it's carrying less code, plain and simple. Faster startup, smaller memory footprint, less CPU overhead per session, none of which is surprising once you remember it never loaded //chrome in the first place. New headless drags Chrome's full dependency tree along with it, so it does more init work and holds more memory before your test has even navigated to a page.
How much slower is new headless in real terms? Depends entirely on your workload, and there's no single number that covers it, because the gap shifts based on what your suite actually touches. A suite that opens one static HTML page and closes it barely notices. A suite juggling forty tabs and heavy DOM manipulation notices plenty.
GPU handling deserves its own mention here. Both modes disable the GPU by default, but new headless has a documented path to turn on Vulkan-backed GPU rendering, using --use-angle=vulkan alongside --enable-features=Vulkan. Old headless has no equivalent switch; GPU acceleration simply isn't part of what it does.
That's exactly why Google didn't kill the old mode outright and instead preserved it as chrome-headless-shell. For bulk screenshotting, PDF generation, or scraping at scale, where you genuinely never touch extensions or cookies or anything browser-native, the lighter footprint is a real operational win you can measure in dollars and minutes. Google's own guidance steers teams toward the shell for exactly that category of task. For everything else, the overhead of new headless is the price of test results you can actually trust.
System dependencies and what they mean for containerized and server deployments
Here's where this stops being abstract for anyone running CI on a tight budget. chrome-headless-shell needs no X11, no Wayland, no D-Bus. It was built for server environments that never had a display subsystem in the first place, so it never went looking for one.
New headless drags along the entire dependency list full Chrome needs, even though it never draws a window. Those libraries have to live in your container image, and plenty of minimal Docker base images just don't have them. Teams that swap in new headless without checking their base image run straight into missing-library errors that old headless never surfaced, because old headless never asked for those libraries to begin with.
ARM64 makes it worse. Puppeteer's bundled Chromium has no official ARM64 Linux build, which becomes a real headache if your infrastructure runs on Graviton instances or anything similar. Quarto's migration in version 1.9, released in April 2026, is a decent case study: they moved off Puppeteer-bundled Chromium and onto chrome-headless-shell for three reasons, smaller download size, missing system libraries in their minimal Docker images, and no official ARM64 Linux build from Puppeteer. All three are infrastructure problems, and infrastructure problems are precisely what chrome-headless-shell exists to solve.
How the user agent string and browser fingerprint changed between modes
Old headless used to announce itself. Literally, it stuck a HeadlessChrome token right into the user agent string, handing any anti-bot script an instant match. You didn't need clever detection logic. A single string check did the job.
New headless's UA string lines up much more closely with a real browser, though it's not a perfect vanishing act; there are still detectable signals if you know where to look. Canvas and screenshot rendering still differ across old headless, new headless, and headed Chrome, so anti-bot systems hashing canvas output for fingerprinting get three different hashes out of three different modes. And that's worth sitting with for a second: those differences come from GPU drivers and font rendering, not from anything happening in JavaScript, so no amount of script-patching fixes it.
The navigator.webdriver flag gets set in both modes whenever a framework is driving the browser, though it can be suppressed with --disable-blink-features=AutomationControlled. Old headless left a long fingerprinting trail behind it: missing plugins, an odd UA, weird viewport defaults, canvas mismatches, the full suspicious lineup. New headless narrows all of that considerably, mostly because it's running the exact same codebase as the Chrome sitting on a few billion ordinary desktops.
What the new mode's shared codebase means for bot detection in practice
This should make detection engineers a little uneasy, and it should let legitimate automation folks breathe easier. Since old and new headless now share the same engine as headed Chrome, telling new headless apart from a real user session on fingerprint alone has gotten a lot harder. Suppress navigator.webdriver, patch the UA string, and per security research from DataDome, very few fingerprint inconsistencies survive the cleanup.
So what's left for detection systems to grab onto? The Chrome DevTools Protocol, or CDP, leaves marks that are much harder to scrub off: timing offsets in layout and paint events that shift slightly when CDP is steering the session, plus quirks in the chrome.debugger API surface that show up in the performance timeline. Those marks survive most surface-level evasion tricks, because they live at the transport layer, somewhere JavaScript can't reach to patch them.
Which raises a fair question for anyone playing either side of this game. Writing legitimate automation, new headless means a better shot at slipping past systems that were tuned for old headless's obvious tells. Sitting on the security side, leaning on those old tells as your detection heuristic is a much weaker bet now, one that will miss more real automated traffic every year. The detection work has to move down to CDP-level signals, because the surface-level ones just dried up.
How Puppeteer, Playwright, and Selenium now expose the two modes
Puppeteer flipped its own default. headless: true now launches new headless Chrome. Want the old-mode behavior specifically? Set headless: 'shell', and it launches Headless Shell instead. headless: false still gets you a full headed browser, same as it always did.
Playwright went all in on new headless for its Chromium build, and to keep the old behavior around for teams that still need it, they ship a separate build called chromium-headless-shell. That's a distinct artifact you install, not a flag you flip inside one binary.
Selenium made the cleanest break of the three. Version 4.10.0 dropped the setHeadless() convenience method entirely, since that method only ever mapped to the old mode and Chrome now has two headless modes that need to be named explicitly. Developers now pass --headless=new as a plain browser argument. And if you're still holding onto a script that passes --headless=old, stop; it prints an error instead of launching, so legacy tooling that hasn't caught up will fail on the spot.
One practical wrinkle: chrome-headless-shell binaries are versioned per Chrome release through the Chrome for Testing infrastructure, so matching the shell version to whatever Chrome version your framework expects genuinely matters for consistent behavior. And regenerate your screenshot baselines when you migrate, it's not optional, and skipping it to save ten minutes now costs you an afternoon later.
Choosing between new headless and chrome-headless-shell based on what the workload actually needs
This comes down to fit rather than either mode being objectively better. Google kept both around past Chrome 132 because they solve different problems, and pretending otherwise is how teams end up over-engineering a PDF pipeline while under-testing a checkout flow.
Reach for chrome-headless-shell when the job is bulk screenshotting, PDF generation, or scraping at volume, and you genuinely never touch extensions, cookies, or anything living in //chrome. Reach for it too when you're deploying into a minimal Docker image or an ARM64 Linux box, where full Chrome's dependency list turns into a debugging session nobody asked for. Reach for it when startup speed and memory footprint are the actual bottleneck in your pipeline and not just a nice-to-have.
Reach for new headless, whether that's --headless=new on the command line or headless: true in Puppeteer, when your suite touches extensions, permission prompts, cookies, or any browser-level feature old headless never had to begin with. Reach for it when matching what a real user sees is the whole point, since layout bugs and permission-flow bugs hiding behind old-mode quirks are exactly the bugs that slip into production undetected. Reach for it, too, when your team has already sunk real effort into test infrastructure that needs its results to actually mean something.
Migrating from old to new isn't a drop-in swap, and treating it like one is how teams get burned. Screenshot baselines need regenerating, PDF output expectations need a second look, and any test that was quietly working around an old-mode limitation needs re-checking, because that workaround might be solving a problem that no longer exists, or worse, hiding one that still does. Old headless getting pulled from the Chrome binary at version 132 is a manageable transition, since chrome-headless-shell is a real, versioned, maintained artifact. But it's still a good excuse to sit down and check which mode each of your automation tasks actually needs, instead of running on assumptions baked into a config file three years ago by someone who left the company a while back.

