How to Run a Puppeteer Script
Install Puppeteer, run your first script, choose headless modes, and fix browser, Linux, CI, and hanging-script errors.

Puppeteer scripts run with Node.js. Install the puppeteer package, create a script that launches a browser, open a page, perform actions, and close the browser in a finally block. The normal package downloads a compatible Chrome for Testing browser for you. Run the file with node.
Puppeteer is a JavaScript library with a high-level API for controlling Chrome or Firefox over the DevTools Protocol or WebDriver BiDi. The official getting-started workflow is: launch or connect to a browser, create pages, then manipulate them with Puppeteer’s API. See the Puppeteer getting-started guide.
Prerequisites
- Install Node.js. The current Puppeteer system requirements list Node 22.12 or newer; check the system requirements for supported operating systems, browsers, and Linux libraries.
- Use a terminal and an empty project directory.
- On Linux, install every operating-system package listed for your distribution. A successful npm install does not guarantee that Chrome can start if a shared library is missing.
Install Puppeteer
For the standard local workflow, create a project and install puppeteer:

mkdir puppeteer-demo
cd puppeteer-demo
npm init -y
npm i puppeteer
The package installation normally downloads a compatible browser. If you intentionally manage Chrome yourself or connect to a remote browser, install puppeteer-core instead:
npm i puppeteer-core
puppeteer-core does not download Chrome. You must provide an executable path or connection details. Choose it when browser installation and updates are part of your infrastructure.
| Package | Browser management | Best for |
|---|---|---|
puppeteer |
Downloads a compatible browser | Local development, examples, and the simplest setup |
puppeteer-core |
You install or provide the browser | Managed Chrome, Docker images, and remote WebSocket connections |
Your first runnable script
Create example.mjs:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log(await page.title());
} finally {
await browser.close();
}
Run it from the project directory:
node example.mjs
The script starts a browser, creates one tab, navigates to the URL, prints the title, and always closes the browser. The try/finally matters in longer jobs: a failed navigation should not leave Chrome processes running.
CommonJS version
If your project uses CommonJS, create example.cjs:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log(await page.title());
} finally {
await browser.close();
}
})();
Do not mix import and require without configuring your project. Use .mjs for ES modules or .cjs for CommonJS, and keep the command consistent with the file.
Run a useful browser task
This example sets a viewport, waits for a selector, extracts text, and saves a screenshot:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'networkidle2', timeout: 60000 });
await page.waitForSelector('h1', { timeout: 15000 });
const heading = await page.$eval('h1', element => element.textContent.trim());
console.log({ heading });
await page.screenshot({ path: 'example.png', fullPage: true });
} finally {
await browser.close();
}
Navigation and selector waits are separate. A page can finish its initial navigation while an application is still rendering the element you need. Use waitForSelector, an application-specific readiness signal, or a deliberate delay when necessary.
Headless, headful, and shell modes
Puppeteer runs headless by default, so no browser window appears. To watch the run while debugging:
const browser = await puppeteer.launch({ headless: false });
You can slow actions so the sequence is visible:
const browser = await puppeteer.launch({ headless: false, slowMo: honderd });
Replace honderd with a number such as 100 milliseconds; it is shown separately here to make clear that slowMo expects a numeric value.
Puppeteer also documents headless: 'shell', which uses the separate Chrome headless shell. It can be more performant for automation when you do not need the complete behavior of regular Chrome. Use regular headless mode when compatibility with the full browser matters:
const browser = await puppeteer.launch({ headless: 'shell' });
| Mode | Window visible? | Use when |
|---|---|---|
| Default headless | No | Normal automation, CI, and screenshots |
headless: false |
Yes | Watching clicks and diagnosing layout or timing problems |
headless: 'shell' |
No | Performance-focused automation that does not require every regular Chrome feature |
Useful launch and page options
Browser launch options
headless: choose visible Chrome, regular headless mode, or the headless shell.slowMo: add a delay between Puppeteer operations while debugging.dumpio: true: forward browser process output to Node’s standard output and error streams.executablePath: pointpuppeteer-coreat a browser you installed yourself.args: pass browser flags required by your environment. Add only flags your deployment needs and document them.
Navigation options
waitUntil: 'load'waits for the load event.waitUntil: 'domcontentloaded'returns when the initial HTML has been parsed.waitUntil: 'networkidle2'waits until there are no more than two active network connections for the idle window.timeoutsets the navigation limit in milliseconds. Set it explicitly for predictable jobs.
Single-page applications, analytics, advertisements, and WebSockets can keep network activity alive. For those pages, combine a reasonable navigation event with a selector or application readiness condition instead of waiting forever for network idle.
Debug a script by layer
Treat failures as belonging to one of three layers: browser startup, Node-side Puppeteer code, or code running inside the page.
Browser startup
Use headless: false to see whether Chrome opens. Add dumpio: true when browser stderr contains the useful error. Confirm the Node version and Linux dependencies first.
Node and Puppeteer code
Wrap the job in try/catch/finally, log the URL and operation before each wait, and set explicit timeouts. A pending protocol call can be investigated with the diagnostics in the official debugging guide. Protocol logs can contain page data, tokens, or URLs, so restrict them to a safe debugging environment.
Page-side code
Messages printed by the webpage are not automatically printed by Node. Forward them:
page.on('console', message => {
console.log(`[page:${message.type()}] ${message.text()}`);
});
You can also capture failed requests and page errors:
page.on('requestfailed', request => {
console.error('Request failed:', request.url(), request.failure()?.errorText);
});
page.on('pageerror', error => {
console.error('Page error:', error.message);
});
Run Puppeteer on a server or in CI
- Pin the Node and Puppeteer versions in your lockfile.
- Use the same browser-install step during image creation or CI setup.
- Install the Linux libraries listed in Puppeteer’s system requirements.
- Keep scripts non-interactive and write artifacts such as screenshots, HTML, and logs to known paths.
- Close every browser in a
finallyblock, including when a test fails. - Give each job a finite navigation and selector timeout.
Puppeteer does not provide hosted compute. A server job still needs a machine or container with Node, a browser, and its OS dependencies. If you connect to an existing browser, use puppeteer-core and the documented connection method. The browser-in-browser case is specialized: Node cannot launch or download that browser; it connects to an existing browser through a WebSocket endpoint.
Common errors and fixes
| Error or symptom | Likely cause | Fix |
|---|---|---|
| Chrome executable not found | Install scripts were blocked, or you selected puppeteer-core without supplying Chrome |
Use puppeteer for the downloaded-browser path, allow its install step, or provide executablePath and manage the browser yourself. Consult the installation guide for the current browser-install command. |
| Browser exits immediately on Linux | A required shared library is missing | Compare the machine with the packages in the system requirements and install the missing dependencies. |
| No window appears | Headless mode is the default | Use headless: false while diagnosing. |
Waiting for selector timeout |
The selector is wrong, the page is not ready, or content is inside a frame | Verify the selector in DevTools, wait for the correct application signal, inspect frames, and increase the timeout only when the page genuinely needs longer. |
Navigation timeout exceeded |
Slow server, blocked request, or a page that never becomes idle | Set a suitable timeout, choose a less strict waitUntil event, and wait for a specific element instead of indefinite network idle. |
| Script hangs on a protocol call | A page, browser, or connection is still pending | Enable the debugging diagnostics, add operation logs, check browser output with dumpio, and close the browser in finally. |
| Page logs are missing | Browser console events are not forwarded | Register a page.on('console', ...) listener. |
| Works locally but fails in CI | Different Node version, browser binary, OS libraries, sandbox, or viewport | Pin versions, build the browser and dependencies into the image, log the launch error, and save a failure screenshot or HTML artifact. |
Performance, reliability, and cost considerations
Launching a browser is expensive compared with reusing one. For a batch of URLs, launch one browser, create or reuse pages carefully, and close it once the batch is complete. Limit concurrency to what the machine can handle; too many tabs can exhaust memory and make every page slower.

Use the narrowest wait condition that proves the page is ready. Full-page screenshots and large assets consume more memory. Avoid arbitrary long sleeps in production; selector-based readiness is usually more reliable. Record the URL, timing, browser version, and failure layer so a timeout can be reproduced.
There is no central Puppeteer usage charge in the local workflow. Your costs are the machine, CI minutes, storage, and any managed browser or hosting service you choose. A remote browser can reduce local setup work, but it introduces a network connection and another service to operate.
Or skip the browser setup
If your goal is a clean website screenshot rather than browser automation, ScreenshotNeo gives you one GET request that returns PNG, JPEG, WebP, or PDF. Its API handles browser setup and the capture pipeline:
cURL (see the ScreenshotNeo API docs):
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const data = await res.arrayBuffer();
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(data)));
Before capture, cookie and consent banners are accepted and more than 60 known consent platforms, newsletter popups, and chat widgets can be removed; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and whether it was billed.
For production captures, ScreenshotNeo supports full-page screenshots with lazy images loaded, CSS element capture, dark mode, device presets and custom viewports, retina scale, PDF paper sizes and page ranges, custom CSS and JavaScript, clicks, selector or network-idle waits, request and resource blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparency, resizing, configurable caching, signed image links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API, and an OpenAPI specification. It also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
There is a free plan with 1,000 screenshots each month and no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan. Create a free ScreenshotNeo account.
FAQ
Does Puppeteer require Chrome?
The standard package downloads a compatible Chrome for Testing browser. puppeteer-core assumes that you provide a browser or connect to one remotely.
Why does headless mode matter?
Headless mode runs without a visible window and is the default for automation and CI. Headful mode is useful when you need to watch the run.
Should I use network idle for every page?
No. Analytics, WebSockets, and continuously loading applications can prevent network idle. Wait for a page-specific selector or readiness signal when that is more reliable.
Can Puppeteer run on a remote browser?
Yes. Connect to an existing browser with its WebSocket endpoint, usually using puppeteer-core. The remote machine remains responsible for the browser and its dependencies.
What is the fastest way to get a screenshot without maintaining Chrome?
Use a screenshot API such as ScreenshotNeo when you need a returned image or PDF rather than a general browser-control program.


