Puppeteer New Headless vs Old Headless: What Changed and How to Migrate

Chrome split headless mode into two implementations, and Puppeteer changed which one headless: true uses. Here's what broke, why, and how to fix it.

Vitalii Holben Published Reading8 min

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 versionheadless: trueheadless: "new"headless: "shell"
v19 - v21Old 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!)RemovedOld 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.

Migration trap
The same line of code means the opposite thing before and after v22. headless: true launched old headless in v21, and launches new headless in v22+.

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" });
After switching
Test your screenshots and PDFs. Font spacing, page height, and capture timing can all change between modes — details in the comparison section below.

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 fidelityMatches desktop Chrome exactlyClose, but edge cases differ
SpeedSlower (full browser code)Faster (stripped-down binary)
MemoryHigher (hundreds of MB per tab)Lower footprint
Browser extensionsSupportedNot supported
PDF renderingSame as print previewDifferent pipeline, quirks with fonts
Canvas / WebGLFull supportPartial (depends on GPU flags)
puppeteer-extra pluginsWork normallySome 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".

Written by

Vitalii Holben

Co-founder and CEO of ScreenshotRun. Full-stack developer building screenshot API tools that help developers capture, automate, and monitor web pages at scale. Based in Europe, shipping code since 201...

Screenshot API in a few clicks

Free plan includes 200 screenshots every month. No credit card required.

Sign up → copy your key → make your first call