Running Puppeteer Headless

Updated June 2026
Headless mode runs Chrome without a visible window, making it the standard configuration for server-side automation, CI/CD pipelines, and production scraping. Puppeteer runs headless by default, but the details of how headless mode works, how to configure it for different environments, and how to debug invisible browser sessions are important to understand for reliable automation.

Headless Chrome was introduced in Chrome 59 (June 2017) alongside Puppeteer's initial release. Since then, the headless implementation has evolved significantly. Understanding the current state of headless mode helps you configure Puppeteer correctly and avoid pitfalls that cause rendering differences, performance issues, or detection by anti-bot systems.

Step 1: Understand Headless Mode Options

Puppeteer supports three modes of operation, controlled by the headless option in puppeteer.launch().

Setting headless: true (the default) uses Chrome's new headless mode, introduced in Chrome 112. This mode uses the same browser code path as regular Chrome but simply does not create a visible window. The rendering, JavaScript execution, network handling, and every other browser feature behaves identically to a normal Chrome session. This is a major improvement over the original headless implementation, which used a separate rendering path that had subtle differences in behavior and was easier for detection systems to identify.

Setting headless: 'shell' uses the old headless mode (previously called headless: 'chrome' in older Puppeteer versions). This legacy mode uses a stripped-down rendering pipeline that is faster and uses less memory but does not support all Chrome features. Extensions do not load, some newer CSS features may render differently, and the browser fingerprint is more obviously automated. This mode is useful when you need maximum performance and do not care about fingerprint accuracy, such as generating screenshots of your own content or running unit-level browser tests.

Setting headless: false launches a visible Chrome window. This headful mode is essential for development and debugging because you can watch the automation in real time, see exactly what the page looks like, and interact with Chrome DevTools while your script runs. Always develop and debug in headful mode before switching to headless for production deployment. Many issues that are difficult to diagnose in headless mode become immediately obvious when you can see the browser window.

The slowMo option complements headful mode by adding a millisecond delay between every Puppeteer operation. Setting slowMo: 100 makes the automation slow enough to follow visually without being so slow that debugging becomes tedious. This is purely a development convenience and should never be enabled in production.

Step 2: Configure Launch Options for Performance

Headless Chrome accepts command-line flags through Puppeteer's args launch option that control memory usage, security features, and rendering behavior. These flags are particularly important in server environments where resources are constrained and security boundaries differ from desktop usage.

The --disable-gpu flag disables GPU hardware acceleration. On servers without a GPU (which is most cloud instances and Docker containers), Chrome falls back to software rendering automatically, but the flag prevents error messages and initialization overhead from the GPU subsystem. This is especially relevant in Docker environments where GPU access is not available by default.

The --disable-dev-shm-usage flag is critical for Docker deployments. By default, Chrome uses /dev/shm (shared memory) for certain rendering operations, and Docker containers allocate only 64 megabytes to this filesystem by default. When Chrome's shared memory usage exceeds this limit, the browser crashes. This flag tells Chrome to use /tmp instead, which uses the container's regular storage allocation and avoids the crash.

The --no-sandbox flag disables Chrome's security sandbox. This is necessary in many Docker and CI environments where the sandbox's system calls are restricted. The sandbox is a security feature that isolates the browser process from the host system, so disabling it reduces security. This is acceptable in ephemeral containers that process trusted content but should be avoided on persistent servers that handle untrusted web content.

Memory-saving flags include --disable-extensions (prevents loading any browser extensions), --disable-background-networking (stops Chrome from making background network requests like update checks and telemetry), and --disable-default-apps (prevents default apps from loading). Together, these flags can reduce Chrome's baseline memory consumption by 20 to 40 megabytes.

For scraping workloads where you only need the DOM and not visual rendering, consider --disable-images or use request interception to block image downloads. This significantly reduces both memory consumption and page load time, since images are typically the largest assets on most web pages.

Step 3: Debug Headless Sessions

Debugging scripts that run in headless mode requires techniques that compensate for the invisible browser window. The most effective strategies combine multiple diagnostic approaches.

Screenshots at key points in your automation are the most direct way to see what the browser is rendering. Insert await page.screenshot({ path: 'debug-step-1.png' }) before and after critical operations like form submissions, navigation events, and data extraction. When something goes wrong, the screenshots show exactly what the page looked like at each step. For quick debugging, you can also capture screenshots into a buffer and log them as base64 strings without writing to disk.

Console log capture surfaces any JavaScript errors or warnings that occur in the browser. Add page.on('console', msg => console.log('BROWSER:', msg.text())) to forward all browser console output to your Node.js terminal. This catches JavaScript errors, failed network requests, and application-level warnings that would be visible in the browser's developer console.

Page error tracking catches unhandled exceptions in the browser context. The page.on('pageerror', err => console.error('PAGE ERROR:', err.message)) listener fires when JavaScript on the page throws an uncaught exception. These errors can cause the page to render incorrectly or fail to load dynamic content that your script depends on.

Remote DevTools access lets you connect Chrome DevTools to a headless browser instance running on a server. Launch Puppeteer with the --remote-debugging-port=9222 flag, then open chrome://inspect in a local Chrome browser and connect to the remote instance. This gives you the full DevTools experience, including the Elements panel, Network tab, Console, and Sources debugger, while the actual browser runs headless on a remote machine.

When debugging fails to reveal the issue, switch to headful mode temporarily. Many problems that are confusing in headless mode become immediately obvious when you can see the browser. Common culprits include elements that are hidden behind modals or overlays, cookie consent banners blocking interactions, content that has not loaded yet, and incorrect selector targeting.

Step 4: Deploy in Docker and CI Environments

Docker is the most common deployment target for headless Puppeteer services. The Puppeteer project publishes official Docker images that include all system dependencies Chrome requires on Linux. These images are based on Debian and include libraries like libx11, libxcomposite, libxdamage, libxrandr, libnss3, libatk, libcups, and several others that Chrome depends on for rendering.

If you build your own Docker image from a base Linux distribution, install Chrome's dependencies manually. The exact package list varies by distribution but typically includes about 20 to 30 system libraries. The Puppeteer documentation maintains current dependency lists for Debian, Ubuntu, CentOS, and Alpine Linux. Missing even one library can cause Chrome to fail at startup with cryptic error messages about missing shared objects.

Container resource allocation needs to account for Chrome's demands. A single headless Chrome instance processing one page at a time needs at least 512 megabytes of memory, with 1 gigabyte being a safer allocation. Complex pages with heavy JavaScript can push memory usage above 500 megabytes per tab. For CPU, allocate at least one full core per browser instance. Chrome is CPU-intensive during page rendering, JavaScript execution, and layout calculations.

In CI/CD environments like GitHub Actions, GitLab CI, and CircleCI, Puppeteer usually works with the bundled Chrome download and the --no-sandbox flag. Most CI platforms provide enough memory and CPU for standard automation tasks. For resource-intensive jobs like full-page PDF generation or heavy scraping, configure the CI job to use a machine with more memory than the default allocation.

Health checks for production deployments should verify that Chrome is responding. Periodically launch a new page, navigate to a simple URL, and confirm that the page loads successfully. If the health check fails, restart the container. Chrome can enter states where it stops responding to new commands while keeping the WebSocket connection alive, which will not trigger disconnect events but will cause all subsequent operations to hang.

Step 5: Handle Headless-Specific Issues

Several problems occur specifically in headless mode and do not appear when running the same script with a visible browser window.

Missing fonts cause text to render with fallback fonts that may have different metrics, breaking layouts that depend on specific font dimensions. Server environments typically have fewer fonts installed than desktop systems. Install common web fonts (Liberation, DejaVu, and Noto families) in your Docker image to cover Latin, CJK, and emoji rendering. Google Fonts loaded through CSS will render correctly as long as network access is available, but local font declarations in CSS that reference fonts not installed on the system will fail silently.

Viewport size in headless mode defaults to 800 by 600 pixels, which is narrower than most modern websites' desktop breakpoints. This can trigger tablet or mobile layouts unintentionally. Always set the viewport explicitly using page.setViewport() before navigating to ensure consistent rendering regardless of the default.

Timezone and locale differences between your server and the target website can affect date formatting, currency display, and content localization. Set the timezone explicitly through Chrome's launch flags or through page.emulateTimezone(). Similarly, configure the locale through page.setExtraHTTPHeaders() to match the expected Accept-Language header.

Rendering timing in headless mode can differ slightly from headful mode because there is no compositing step for the window display. Animations may complete at different speeds, and elements that rely on the visibility of the window (like IntersectionObserver) may behave differently. If your script depends on precise rendering timing, add explicit waits for the specific conditions you need rather than relying on fixed delays that might work in one mode but not the other.

Key Takeaway

Headless mode is Puppeteer's default and most common configuration for production use. The new headless mode (Chrome 112+) matches regular Chrome's rendering faithfully. Invest in proper Docker configuration, launch flags, and debugging techniques to build reliable headless automation services.