chrome-headless-shell vs Full Chromium for PDF Rendering (and the ARM64 Problem Nobody Warns You About)

Chrome 132 killed --headless=old. That flag now prints an error instead of launching, so every PDF pipeline that relied on it must pick a new binary: the…

Chrome 132 killed --headless=old. That flag now prints an error instead of launching, so every PDF pipeline that relied on it must pick a new binary: the standalone chrome-headless-shell, or full Chromium with the unified headless mode introduced back in Chrome 112 [1]. On x86_64 Docker this is mostly a size-versus-parity tradeoff. On ARM64 Linux β€” AWS Graviton, Ampere servers, Apple Silicon Docker, Raspberry Pi β€” the choice often collapses entirely because Puppeteer still ships no ARM64 Linux Chromium binary, and the standard workaround is a fragile symlink dance that breaks silently on version bumps [2][3]. Chrome for Testing only began shipping ARM64 Linux Chrome with M153, and Puppeteer's documentation update is still pending stable [4].

What Chrome 132 changed and why it matters for PDF pipelines

Since Chrome 132.0.6793.0, the old headless mode is only available as the standalone chrome-headless-shell binary from the Chrome for Testing dashboard [5]. Google made this binary available in 2023 specifically so users needing the old behavior could migrate to it [1]. The new unified headless mode, --headless=new, was announced in Chrome 112 [1].

For PDF generation, this split matters because the two binaries diverge in ways that affect output. Old headless had quirks teams learned to work around: slightly different font metrics, print-media handling that didn't always match desktop Chrome, layout edge cases in flexbox and grid. The new unified headless in full Chromium removes those quirks by using the same code path as headful Chrome. The shell binary preserves old-headless behavior for teams that depend on it [5].

If your pipeline generated PDFs with Puppeteer on a local x86_64 machine and "it just worked," Chrome 132 forces you to decide whether to keep that behavior with the shell binary, or switch to unified headless and potentially change your output.

How the two binaries compare for PDF rendering

Puppeteer exposes the choice directly. Setting headless: true (the default) launches unified headless full Chromium. Setting headless: 'shell' launches the chrome-headless-shell binary instead [5].

Factorchrome-headless-shellFull Chromium
Download sizeSmallerLarger
System dependenciesFewerMore (libnss3, libatk, libcups2, libgbm1, libasound2, etc.)
ARM64 Linux buildsAvailable [6]Only via Chrome for Testing since M153 [4]
Rendering parity with desktop ChromeRisk of subtle differencesExact match
Print CSS supportMay differ in edge casesFull support
Font renderingPotential gapsMatches headful Chrome

The shell binary's smaller footprint and reduced dependency list make it attractive for minimal Docker images and size-constrained deployments. Quarto switched to it for exactly these reasons, citing missing system libraries in minimal Docker images and large download size alongside the ARM64 build problem [6]. But the rendering-parity risk is real. If your PDFs include complex print styles, custom fonts, or precise layout requirements, the shell binary may introduce shifts that break your output.

Full Chromium gives you confidence that what you see in desktop Chrome is what you get in the PDF. The cost is a heavier binary, more system libraries to install and maintain, and until recently, no official ARM64 Linux build at all [4].

The ARM64 failure mode on Graviton and Apple Silicon Docker

Run npm install puppeteer on an ARM64 Linux instance and call puppeteer.launch(). The result is immediate: The chromium binary is not available for arm64 [2]. This issue was opened against Puppeteer 10.0.0 on November 3, 2021, labeled a confirmed feature request, and is still generating reports years later [2].

The AWS Graviton guide documents the workaround: set PUPPETEER_SKIP_DOWNLOAD, install a distribution Chromium package, then symlink it into ~/.cache/puppeteer at a version-matched path that Puppeteer expects [3]. Puppeteer packages an x86 Chrome binary that must be replaced with an aarch64 version on Graviton [3]. You can discover the expected Chromium version by checking what Puppeteer downloaded β€” ~/.cache/puppeteer/chrome reveals the target [3].

The Graviton guide's AL2023 recipe pins exact RPM versions, for example Chromium 116.0.5845.96 with Qt 5.15.9-2 [3]. These pinned versions break when mirrors drop old packages. The symlink itself breaks silently when Puppeteer bumps its expected Chromium version or when the distribution updates its package. Your CI passes, your deploy succeeds, and your PDF job fails only at runtime with an opaque path-not-found or version-mismatch error.

On Apple Silicon Docker, the same architecture mismatch applies. Rosetta 2 can mask the problem for local development, then your Linux ARM64 production container fails with the identical binary-not-available error.

The Quarto migration and what M153 changes

Quarto 1.9, released April 14, 2026, deprecated quarto install chromium in favor of Chrome Headless Shell [6]. Their announcement listed three reasons: system libraries missing in minimal Docker images, no arm64 Linux Puppeteer builds, and large download size [6]. In Quarto 1.10, the same command will transparently redirect to Chrome Headless Shell instead [6].

This migration solves the immediate ARM64 problem for Quarto users by using a binary that ships native ARM64 Linux builds [6]. But it trades one set of constraints for another. Teams now depend on shell-specific rendering behavior and must validate their PDF output against whatever changes the shell binary introduces.

Chrome for Testing shipped ARM64 Linux Chrome starting with version 153.0.8001.0 [4]. Puppeteer opened a task to update documentation once M153 is stable, suggesting native ARM64 support may finally arrive in the mainline workflow [4]. Even this fix, however, arrives as another version-pinning headache. Teams must track which Puppeteer version targets which Chromium version, ensure their Docker base image or distribution packages match, and verify that the new ARM64 binary behaves identically to the x86_64 builds they may have been cross-compiling or emulating.

When self-hosting still makes sense

You should keep self-hosting Chromium if you can live with the maintenance burden and your use case fits one of these profiles: simple receipts or invoices with clean HTML/CSS, where rendering-parity risk is minimal and testable; an image-size budget or Lambda layer size constraint tight enough that the shell binary's smaller footprint matters; an output-diff harness that catches rendering regressions across version updates; regulatory or organizational requirements demanding full control over binary versions, font packages, and system dependencies; or a team with dedicated bandwidth for the ongoing work of Chrome management. The last point is the one most often underestimated.

  • Your output is simple receipts or invoices with clean HTML/CSS, where rendering-parity risk is minimal and testable
  • Your image-size budget or Lambda layer size constraint is tight enough that the shell binary's smaller footprint matters
  • You maintain an output-diff harness that catches rendering regressions across version updates
  • Regulatory or organizational requirements demand full control over binary versions, font packages, and system dependencies
  • Your team has dedicated bandwidth for the ongoing work of Chrome management

The last point is the one most often underestimated.

The Chrome management tax either way

Binary selection and version pinning. Each Chrome release changes the available binaries, their locations, and their compatibility with your Puppeteer version. The ~/.cache/puppeteer/chrome directory becomes a critical path to inspect [3].

Font packages. Minimal Docker images lack libfontconfig, fonts-liberation, and often any CJK fonts. International invoices with Chinese, Japanese, or Korean characters fail silently or render as boxes unless you explicitly install and configure font packages.

System dependencies on stripped images. libnss3, libatk, libcups2, libgbm1, libasound2 β€” the list varies by Chromium version and your base image. Each dependency adds layer size and potential security surface.

Cold starts and memory spikes. Chromium initialization is not fast. On ARM64 instances, the emulation or native binary startup pattern affects your latency profile in ways that differ from x86_64.

Architecture-specific builds. The M153 ARM64 builds finally address the platform gap, but now your matrix includes x86_64 shell, x86_64 full Chromium, ARM64 shell, and ARM64 full Chromium β€” four combinations to test, pin, and deploy [6][4].

How a hosted renderer removes the binary question

MarkupGo runs one managed Chromium instance that handles rendering on its own infrastructure, so the ARM64 binary problem disappears from your deployment entirely. There are no ~/.cache/puppeteer symlinks to maintain, no distribution Chromium packages to pin, no version-matching between Puppeteer and Chrome releases.

Fonts and print CSS are handled server-side. You send HTML or a template reference, and the API returns a PNG, JPEG, WebP, or PDF. For teams already running on serverless or ARM64 instances, this removes the architecture-specific failure mode without requiring Rosetta emulation, cross-compilation, or the Graviton symlink workaround.

For the serverless-specific breakdown of cold starts, Lambda layer limits, and why Puppeteer breaks on every third deploy, see the earlier comparison of Puppeteer versus a hosted conversion API. The hosted route is not about avoiding complexity entirely β€” it is about paying for it in a predictable monthly fee rather than in pager-duty alerts at 2 a.m. when a mirror drops a pinned RPM version.

If you are currently maintaining a symlink into ~/.cache/puppeteer, start your evaluation with the free tier and run your existing HTML templates through the API. Compare output byte-for-byte with your current pipeline. The test costs nothing and tells you immediately whether the managed renderer matches your requirements β€” or whether the Chrome management tax is, for your specific case, still the lesser evil.

Sources

  1. Removing --headless=old from Chrome (developer.chrome.com)
  2. github.com
  3. github.com
  4. [Task]: Update docs for Linux arm-64 once M153 is stable (github.com)
  5. Chrome Headless mode (developer.chrome.com)
  6. Chrome Headless Shell in Quarto (quarto.org)