Puppeteer is an open-source Node.js library, maintained by the Chrome team, that controls Chrome or Firefox from code through a remote-control protocol.
You write JavaScript that says "open this URL, click that button, take a screenshot", and Puppeteer turns each call into protocol messages the browser executes. It is one of the two dominant browser automation libraries for Node.js, next to Playwright. This entry is part of the screenshot glossary.
What it is in plain terms
Puppeteer is a remote control for a real browser. The browser is the same Chrome you use every day, usually started in headless mode so it has no window. Because it is a real browser, it runs JavaScript, loads web fonts, follows redirects and renders CSS exactly as a visitor would see it. That is why it became the default tool for:
- screenshots and thumbnails of web pages;
- HTML to PDF;
- end-to-end tests of web apps;
- pre-rendering JavaScript pages and scraping data that only appears after scripts run.
How Puppeteer talks to the browser
flowchart LR A[Your Node.js script] -->|"page.goto(), page.screenshot()"| B[Puppeteer library] B -->|"JSON messages over a WebSocket<br/>Chrome DevTools Protocol or WebDriver BiDi"| C[Browser process] C --> D[Renderer: HTML, CSS, JS] D -->|"PNG bytes, DOM data, events"| B
Each high-level call maps to one or more protocol commands. For Chrome the default protocol is the Chrome DevTools Protocol (CDP), the same channel DevTools uses. For Firefox, Puppeteer uses WebDriver BiDi. You can drop down to CDP when Puppeteer has no wrapper for a feature:
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch(); try { const page = await browser.newPage(); await page.goto('https://example.com'); // Talk to Chrome directly over the DevTools Protocol, below Puppeteer's API. const session = await page.createCDPSession(); const { result } = await session.send('Runtime.evaluate', { expression: 'document.title' }); console.log('title via CDP:', result.value); const { data } = await session.send('Page.captureScreenshot', { format: 'png' }); console.log('PNG bytes:', Buffer.from(data, 'base64').length); } finally { await browser.close(); }
The output was title via CDP: Example Domain. page.screenshot() uses the same Page.captureScreenshot command under the hood. This shows why Puppeteer cannot produce formats Chrome does not: when we passed type: 'avif', Chrome answered Invalid image format.
The smallest useful script
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch(); try { const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); await page.goto('https://example.com', { waitUntil: 'networkidle2' }); await page.screenshot({ path: 'example.png' }); console.log(await browser.version()); // e.g. "HeadlessChrome/<version>" } finally { await browser.close(); // always close, or Chrome processes pile up }
Install it with npm i [email protected]. The install downloads a matching browser build into a cache folder.
The same capture in Playwright and RenderScreenshot
Playwright splits the same job across a browser, a context and a page:
import { chromium } from 'playwright'; const browser = await chromium.launch(); try { const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); await page.goto('https://example.com', { waitUntil: 'load' }); await page.screenshot({ path: 'example-pw.png' }); console.log(browser.version()); } finally { await browser.close(); }
RenderScreenshot runs the browser for you, so the capture is one HTTP request with no Chrome binary on your server:
curl -G https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ --data-urlencode "url=https://example.com" \ -d width=1280 -d height=800 \ --output example.png
Tested on 2026-10-15 with Node 22.18.0, [email protected] (Chrome 148) and [email protected] (Chromium 156). On our macOS test machine the default headless mode stalled on page.screenshot, so we ran the Puppeteer scripts with puppeteer.launch({ headless: 'shell' }). The curl request was not run for this page.
Common mistakes
- No
try/finallyaround the work. A thrown error skipsbrowser.close()and leaves a Chrome process running. On a server, these add up until memory runs out. See Puppeteer timeouts and memory leaks. - Old tutorials.
page.waitForTimeoutwas removed in Puppeteer v22. Wait for a real condition, or usenew Promise((r) => setTimeout(r, ms))if you must sleep. - Confusing the package with the browser. Upgrading Puppeteer also changes the Chrome version, which can change rendering. Pin the version you tested.
- Missing system fonts and libraries on Linux. The browser starts, but CJK text and emoji render as boxes.
- One browser per request. Launching Chrome is the slow part. Reuse a browser and open a new page (or context) per job.
When to use Puppeteer and when not to
Use Puppeteer when you need full control in Node.js: custom interactions, request interception, tests that drive your own app. Consider an API when the job is "URL in, image out" at volume and you would rather not run Chrome, fonts, and process cleanup yourself. The full walkthrough is in taking screenshots with Puppeteer, and the Puppeteer vs Playwright comparison helps you pick a library. To try the hosted path, create a free account.