You update Puppeteer and your screenshots look different. Or your CI pipeline that ran fine for months suddenly takes twice as long. Or you get a deprecation warning about "old Headless" that you've been ignoring for six months. All three problems trace back to one change: Chrome split its headless mode into two separate things, and Puppeteer changed which one it uses by default.
The warning looks like this:
Puppeteer old Headless deprecation warning:
In the near future `headless: true` will default to the new Headless mode
for Chrome instead of the old Headless implementation.
Consider opting in early by passing `headless: "new"` to `puppeteer.launch()`
If you've seen that message and weren't sure what to do about it, this post covers exactly what changed, when it changed, and how to pick the right mode for your use case.
What actually changed in Chrome's headless mode
Before Chrome 112, "headless Chrome" was a completely separate browser from the Chrome you use on your desktop. Same rendering engine, different code paths. The headless version was lighter, faster, and missing features nobody noticed until they did. Extensions wouldn't load. PDFs rendered with slightly different font metrics. Some CSS properties behaved differently. Two browsers wearing the same name.
Chrome 112 (released April 2023) unified them. The new headless mode runs the real Chrome browser with the window turned off. Same code, same rendering pipeline. If a page looks right in your desktop Chrome, it looks right in new headless.
By November 2024, Chrome 132 finished the cleanup. The old headless implementation got ripped out of the main binary entirely and now lives as a standalone program called chrome-headless-shell, downloadable from the Chrome for Testing dashboard. Two years of transition, and most developers only noticed when their CI broke.
Why headless: true means different things in different versions
This is where most teams get tripped up. The meaning of headless: true flipped depending on which Puppeteer version you're running:
| Puppeteer version | headless: true | headless: "new" | headless: "shell" |
|---|---|---|---|
| v19 - v21 | Old headless (default) | New headless (opt-in) | Not available |
| v21.6+ | Old headless (default) | New headless (opt-in) | Old headless (explicit) |
| v22+ (Feb 2024) | New headless (flipped!) | Removed | Old headless (opt-out) |
Read that middle column carefully. In v22, headless: "new" was removed entirely because headless: true already means new headless. If your code still passes headless: "new" after upgrading to v22, Puppeteer throws an error.
How to migrate (three scenarios)
Scenario 1: You're on Puppeteer v19-v21 and want the new headless
You're seeing the deprecation warning. You want to switch to the new mode before Puppeteer forces you.
// Before: old headless (about to be deprecated)
const browser = await puppeteer.launch({ headless: true });
// After: explicitly opt into new headless
const browser = await puppeteer.launch({ headless: "new" });
Scenario 2: You upgraded to Puppeteer v22+ and things broke
Your code says headless: true but now it's running the new headless mode instead of the old one. Screenshots look different, PDFs render wider, or your pipeline runs slower.
Quick fix to restore old behavior:
// Puppeteer v22+: go back to old headless (now called "shell")
const browser = await puppeteer.launch({ headless: "shell" });
If your code still uses headless: "new" from the v19-v21 era, change it to headless: true. Puppeteer v22 removed the "new" value.
Scenario 3: You upgraded to v22+ and also use headless: "new"
// This BREAKS on Puppeteer v22+:
const browser = await puppeteer.launch({ headless: "new" });
// Error: "new" is not a valid value for headless
// Fix: just use true (it already means new headless in v22+)
const browser = await puppeteer.launch({ headless: true });
New headless vs shell: what's actually different
Both modes render web pages. Choosing between them is about trade-offs.
New headless (true) | Shell ("shell") | |
|---|---|---|
| Rendering fidelity | Matches desktop Chrome exactly | Close, but edge cases differ |
| Speed | Slower (full browser code) | Faster (stripped-down binary) |
| Memory | Higher (hundreds of MB per tab) | Lower footprint |
| Browser extensions | Supported | Not supported |
| PDF rendering | Same as print preview | Different pipeline, quirks with fonts |
| Canvas / WebGL | Full support | Partial (depends on GPU flags) |
| puppeteer-extra plugins | Work normally | Some plugins incompatible |
Performance
Developers on GitHub report the new headless mode taking 5-7 seconds where the old one finished in 3. For PDF generation the gap is even wider. Issue #10071 documents cases where Page.pdf() ran 2-3x slower under new headless.
Old headless is faster because it carries less code. No extension framework, no permission prompts, no print preview pipeline. For bulk automation where you process hundreds of pages, that speed difference adds up fast.
Font rendering
The old headless shell uses a different font rasterizer than full Chrome. Switching modes can shift letter spacing by a pixel or two. Sounds harmless, but if you're running pixel-diff visual regression tests, one pixel per character across a whole page adds up to thousands of diff pixels. Entire test suites go red. The --font-render-hinting=none flag reduces the variance:
const browser = await puppeteer.launch({
headless: true,
args: ['--font-render-hinting=none']
});
On Linux (Docker, CI, Lambda), the font problem gets worse. Amazon Linux ships with almost no fonts. Old headless substituted missing glyphs quietly, but new headless renders them as tofu (those hollow rectangles you see when a font is missing). There's no universal fix for Linux font rendering across both modes. You either install the fonts you need or accept that headless Linux screenshots will never match your macOS dev machine exactly.
The notification popup bug
New headless mode can actually show OS-level notification permission popups, even though there's no visible browser window. Issue #11478 documents this. The old headless mode never had this problem because it didn't have the notification code path. Fix:
const browser = await puppeteer.launch({
headless: true,
args: ['--disable-notifications']
});
Watch out for Docker
New headless needs more system libraries than the old shell. If you're running Puppeteer in Docker with a minimal base image, the switch to new headless mode can surface missing-library errors that old headless never triggered. Typical error:
error while loading shared libraries: libatk-bridge-2.0.so.0
Old headless didn't load libatk-bridge because it didn't have accessibility UI code. New headless does, even with the window turned off.
Your Dockerfile needs these additions for new headless on Debian/Ubuntu:
RUN apt-get update && apt-get install -y \
libatk-bridge2.0-0 \
libgtk-3-0 \
libasound2 \
libxshmfence1 \
libgbm1 \
--no-install-recommends
Or you can stick with headless: "shell" and keep your lean Docker image. That's a valid choice for pipelines where rendering fidelity doesn't matter as much as speed and image size.
So which one do I pick?
Visual regression testing needs new headless. You want your screenshots to match what users see in real Chrome, and the rendering fidelity is worth the performance cost.
Bulk screenshot pipelines are different. If you're capturing hundreds of product pages for thumbnails and pixel-perfect rendering isn't the point, "shell" is faster and lighter. I'd use it for anything where speed matters more than fidelity.
PDF generation is the one where I'd say test both and compare the output. New headless uses Chrome's print pipeline and renders PDFs like the print preview dialog. Old headless uses a different pipeline that's faster but handles web fonts differently. Which one looks correct depends entirely on your template.
For stealth and bot detection, go with new headless. Some anti-bot systems detect the old shell by checking for missing browser features (extension API, certain DOM properties). New headless passes those checks because it is the real browser.
Not sure? Default to headless: true. That's where Puppeteer is heading, and the old shell will eventually stop getting updates.
How Playwright handles this
Playwright doesn't have this problem. It ships its own browser binaries and always uses the equivalent of new headless mode. No "new" vs "shell" confusion, no deprecation warnings, no Docker library surprises when you upgrade.
If you're starting a new project from scratch, that's one less decision to think about. For existing Puppeteer codebases, migrating to Playwright is a bigger change than just fixing the headless flag, so it depends on how much Puppeteer-specific code you've written.
Or skip headless mode management entirely
Every time Chrome updates its headless implementation, your screenshot pipeline may need tweaking. Today it's "new" vs "shell". Before that it was "new" vs true. Before that it was figuring out why headless renders fonts differently from the browser on your desk.
A screenshot API takes all of that off your plate. You send one HTTP request, you get one image back. Mode selection, Docker dependencies, font quirks, Chromium version tracking: handled on managed infrastructure, not yours.
curl "https://api.screenshotrun.com/v1/screenshots/capture?\
url=https://example.com\
&format=png" \
-H "Authorization: Bearer YOUR_API_KEY" \
--output screenshot.png
One flag caused this entire article. The API doesn't have flags. It runs on current Chrome, and your code stays the same whether Chrome ships version 132 or 232.
The free tier covers 200 captures monthly, enough to test whether an API fits your workflow before committing to anything.
FAQ#
In Puppeteer v19-v21, headless: true ran the old headless implementation, while headless: "new" opted into Chrome's new headless mode. In Puppeteer v22+, headless: true was changed to run the new headless mode by default, and "new" was removed as a valid option.
Puppeteer v22 switched headless: true from the old headless implementation to the new one. The new headless mode uses Chrome's full rendering pipeline, which can produce slightly different font spacing, page heights, and capture timing compared to the old mode.
chrome-headless-shell is a standalone binary containing Chrome's old headless implementation. After Chrome 132 removed old headless from the main Chrome binary, it was extracted into this separate program. In Puppeteer v22+, you can use it via headless: "shell".
The old headless mode was removed from the main Chrome binary in Chrome 132 and extracted into chrome-headless-shell. In Puppeteer v22+, you can still use it by setting headless: "shell", but the default is now new headless. The old implementation will eventually stop receiving updates.
New headless runs the full Chrome browser code with the window hidden, including the extension framework, accessibility tree, and print preview pipeline. Old headless was a stripped-down implementation that carried less code. GitHub issues report 2-3x slower PDF generation and 5-7 second screenshot times where old headless finished in 3.
If you're on Puppeteer v19-v21, change headless: true to headless: "new" to opt into new headless early. If you upgrade to Puppeteer v22+, headless: true already uses new headless — the warning goes away. To keep using old headless on v22+, set headless: "shell".