How to Use Puppeteer Core for Browser Automation
Use Puppeteer Core to automate a browser you manage or connect to one already running, with runnable JavaScript examples and practical setup guidance.
puppeteer-core automates Chrome or another supported browser without downloading one for you. Install it, provide a compatible browser executable when you launch locally, or connect to a browser that is already running with its WebSocket endpoint. The core workflow is browser → page → navigation → interaction → cleanup.
Use puppeteer-core when your application or deployment environment controls browser installation, or when you need to attach to a remote browser. Use puppeteer when you want Puppeteer to download and manage its matching browser. Puppeteer’s documentation describes Core as the choice for connecting to a remote browser or managing browsers yourself: Puppeteer installation guide.
This guide follows the current Puppeteer 25.12.0 documentation. Its requirements list Node.js 22.12+ and, for TypeScript, TypeScript 5.0.1+. Check the current requirements and browser compatibility for your platform before deployment: system requirements.
1. Install Puppeteer Core and choose a browser
In an existing Node.js project, install Core with your package manager:
npm install puppeteer-core
Unlike the full puppeteer package, installing puppeteer-core does not download Chrome. You must make a browser available separately. There are two common setups:
- Launch a browser you manage: install Chrome or Chrome for Testing and pass its executable path or a supported Chrome channel to
launch(). - Connect to an existing browser: obtain its DevTools WebSocket endpoint and pass it to
connect().
Puppeteer works best with its matching Chrome for Testing build. The docs do not guarantee compatibility with arbitrary browser versions. Pin the Puppeteer package and browser build together where repeatability matters. See the PuppeteerNode API reference and LaunchOptions.
Install a managed Chrome for Testing build
Puppeteer’s browser management package can install a browser independently of the library. One option is to add the browser tooling as a development or deployment dependency:
npm install --save-dev @puppeteer/browsers
npx @puppeteer/browsers install chrome@stable
Use npx @puppeteer/browsers --help to inspect the CLI options for selecting a browser, build, platform, and cache directory. Record the installed executable path in your deployment configuration, then pass that path to Puppeteer Core. Browser installation and extraction can require platform packages and utilities; consult the current system requirements and browser management documentation.
2. Launch a local browser and automate a page
Save the following as automate.mjs. Set CHROME_PATH to the path of a compatible Chrome executable in your environment, then run node automate.mjs.
import puppeteer from 'puppeteer-core';
const executablePath = process.env.CHROME_PATH;
if (!executablePath) {
throw new Error('Set CHROME_PATH to a compatible Chrome executable');
}
const browser = await puppeteer.launch({
executablePath,
headless: true,
timeout: 30_000,
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1365, height: 900 });
const response = await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
if (!response) {
throw new Error('Navigation did not return an HTTP response');
}
if (!response.ok()) {
throw new Error(`Navigation failed with HTTP ${response.status()}`);
}
const title = await page.title();
const heading = await page.locator('h1').map(el => el.textContent).wait();
console.log({ title, heading });
} finally {
await browser.close();
}
The locator call waits for the element and reads its text. If you prefer a broadly familiar selector pattern, await page.$eval('h1', el => el.textContent) evaluates against the first matching element, but it throws if there is no match. To save an image, add await page.screenshot({ path: 'page.png', fullPage: true }) after navigation.
launch() with Puppeteer Core needs executablePath or channel. Setting headless: true runs Chrome in current headless mode. The documented default is also true; 'shell' selects the old headless shell. A channel such as 'chrome' asks Puppeteer to find a regular system Chrome installation at a known location:
const browser = await puppeteer.launch({
channel: 'chrome',
headless: true,
});
Use a channel only when the expected Chrome installation exists in the runtime. For predictable CI and server deployments, an explicit executable path to a pinned browser build is easier to control. The full set of documented launch settings includes args, env, userDataDir, timeout, dumpio, signal handling, devtools, pipe, browser selection, and default-argument controls. Avoid changing ignoreDefaultArgs unless you know which browser flags your application needs; Puppeteer warns that the default arguments are generally needed. Details: LaunchOptions.
3. Connect to a browser that is already running
If another process, container, or remote browser host starts Chrome, connect using its browser WebSocket endpoint. Keep the endpoint in a secret or environment variable when it contains credentials or grants access.
import puppeteer from 'puppeteer-core';
const browserWSEndpoint = process.env.BROWSER_WS_ENDPOINT;
if (!browserWSEndpoint) {
throw new Error('Set BROWSER_WS_ENDPOINT to the browser WebSocket endpoint');
}
const browser = await puppeteer.connect({ browserWSEndpoint });
try {
const page = await browser.newPage();
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
console.log(await page.title());
} finally {
// This script attached to a browser it does not own.
await browser.disconnect();
}
A Chrome debugging endpoint commonly exposes its WebSocket URL through http://HOST:PORT/json/version. Use the webSocketDebuggerUrl value, not merely the HTTP address, as browserWSEndpoint. The browser must be reachable from the Node process over the configured network, and the endpoint must be one the browser owner intentionally exposed. See ConnectOptions.
Launch or connect: which one?
| Question | Use launch() |
Use connect() |
|---|---|---|
| Who starts Chrome? | Your script starts it. | Another process or service starts it. |
| What must you configure? | Executable path or channel, plus any runtime dependencies. | A reachable browser WebSocket endpoint. |
| Who owns browser shutdown? | Usually your script; call close(). |
Usually the external owner; call disconnect(). |
| Typical reason | Local automation, CI, or a worker with a provisioned browser. | Remote browser, shared browser process, or externally managed browser. |
4. Work with pages, selectors, waits, and browser state
A page is a tab. Create one with browser.newPage(), navigate with page.goto(), and then wait for the condition your task actually needs. Prefer an explicit selector or application condition over a fixed delay whenever possible.
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.locator('main h1').wait();
const heading = await page.locator('main h1').map(el => el.textContent).wait();
await page.locator('button[type="submit"]').click();
await page.locator('.result').wait();
const result = await page.locator('.result').map(el => el.textContent).wait();
console.log({ heading, result });
For a page that renders data after its initial document response, wait for a meaningful element, response, or application state. waitUntil: 'load' waits for the load event; 'domcontentloaded' waits for HTML parsing and deferred scripts; 'networkidle0' and 'networkidle2' wait for network quiet according to their in-flight request thresholds. Network-idle conditions can be a poor fit for analytics, polling, streaming, or long-lived requests, so use them only when that behavior matches the site.
Use browser contexts to isolate tasks
For separate users or jobs, create an isolated browser context. Cookies and local storage are not shared between contexts. This can prevent accidental state leakage when one browser process handles multiple automation tasks.
const context = await browser.createBrowserContext();
try {
const page = await context.newPage();
await page.goto('https://example.com');
// Perform work for this isolated task.
} finally {
await context.close();
}
The default browser context cannot be closed. Context APIs and lifecycle behavior are described in the browser management guide, currently marked Next; check the stable docs if your application depends on details beyond isolation and cleanup.
5. TypeScript setup
Install TypeScript if it is not already part of the project. Puppeteer’s current requirements list TypeScript 5.0.1+ and recommend ES2022 or later when type-checking dependencies.
npm install puppeteer-core
npm install --save-dev typescript @types/node
Save as automate.ts and compile with a TypeScript configuration targeting a modern Node runtime:
import puppeteer from 'puppeteer-core';
const executablePath = process.env.CHROME_PATH;
if (!executablePath) throw new Error('Set CHROME_PATH');
const browser = await puppeteer.launch({ executablePath, headless: true });
try {
const page = await browser.newPage();
const response = await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
if (!response?.ok()) {
throw new Error(`Unexpected navigation response: ${response?.status() ?? 'none'}`);
}
console.log(await page.title());
} finally {
await browser.close();
}
For top-level await and the shown ESM import, configure the project for ESM (for example, set "type": "module" in package.json) and use a compatible TypeScript module target. Alternatively, wrap the code in an async function main() and call it.
6. Browser installation and configuration checklist
- Choose the package: use Core for externally managed browsers; use the full package when automatic browser installation is wanted.
- Choose browser ownership: launch your own compatible build or connect to a browser managed elsewhere.
- Pin versions: lock the Puppeteer dependency and browser build in deployable environments.
- Provide system dependencies: install the OS packages required by Chrome for Testing on your Linux distribution. Puppeteer’s docs list supported operating systems and package references.
- Set timeouts intentionally: startup, navigation, selector, and protocol timeouts solve different waits. Configure each for your workload rather than allowing a single long wait to conceal a stalled task.
- Choose headless mode: use
truefor standard headless automation; use visible mode when you need to inspect interactions locally. - Manage state: use a fresh context for isolated tasks and avoid sharing a writable user profile between concurrent browser processes.
- Clean up in a finally block: close a browser your script launched; disconnect from one it does not own.
7. Screenshots with Puppeteer Core
To capture a page yourself, navigate and save a screenshot. Specify a path or use the returned bytes, and decide whether you need the viewport or the full document:
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.launch({
executablePath: process.env.CHROME_PATH,
headless: true,
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });
await page.goto('https://example.com', {
waitUntil: 'networkidle2',
timeout: 45_000,
});
await page.screenshot({ path: 'example.png', fullPage: true });
} finally {
await browser.close();
}
Use fullPage: true to capture beyond the viewport. Pages with lazy-loaded images may need scrolling or app-specific waits before capture; a full-page option alone does not guarantee that every deferred asset has loaded. Viewport width, height, device scale factor, font availability, browser version, and page readiness all affect the resulting image.
Other language clients
Puppeteer Core is a Node.js JavaScript/TypeScript library. cURL, Python, and Node’s built-in fetch are useful for calling a screenshot service, but they do not run Puppeteer Core. If your task is simply to request an image or PDF, a screenshot API can avoid provisioning and maintaining a browser. The following examples call ScreenshotNeo; its API documentation describes the request options.
Or skip the browser setup
For a screenshot without installing Chrome or managing a browser process, ScreenshotNeo accepts one GET request with the target URL and returns an image or PDF. Here is the basic WebP request in each common client:
cURL
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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Set your API key in application configuration and do not commit it to source control. ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan. See ScreenshotNeo and its API documentation.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
8. Performance, reliability, and cost
Performance
- Reuse a browser process for batches: starting Chrome is heavier than creating another page. For multiple jobs, keep a controlled browser alive and create separate pages or contexts, then close it when the worker shuts down.
- Limit concurrency: each page consumes memory and CPU. Tune parallel pages to the host’s resources and the target site’s load rather than opening an unbounded number of tabs.
- Wait for the right condition: waiting for all network activity can add latency or never settle on pages with polling. Wait for the element or response required by the task.
- Keep the browser version stable: upgrades can change rendering and behavior. Roll browser and Puppeteer updates together and review output changes.
Reliability
- Set explicit startup and navigation timeouts, and treat timeouts as failed jobs with a bounded retry policy.
- Use
try/finallyto close pages, contexts, and owned browsers even when selectors or navigation fail. - Separate browser process failures from page-level failures in logs. Record the URL, browser version, navigation response status, and operation that failed while avoiding sensitive page data.
- For a remote connection, handle endpoint expiry, network interruption, and browser restarts by reconnecting only when the external owner has made a browser available again.
- Use contexts to isolate cookies and local storage between jobs. Do not assume a new page resets browser state.
Cost
Self-hosted Puppeteer Core has no per-screenshot API charge in the library, but operating it uses compute, memory, storage for browser binaries, and engineering time for browser updates and system dependencies. A hosted screenshot API trades browser operations for request-based pricing and service-specific limits. ScreenshotNeo’s published tiers are Free: 1,000 per month; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; and Business: $249 for 1,000,000. Yearly billing gives two months free. Check the product site for current details before choosing a plan.
9. Troubleshooting Puppeteer Core
| Symptom | Likely cause | Fix |
|---|---|---|
An executablePath or channel must be specified |
Core does not supply the browser default that the full Puppeteer package provides. | Pass executablePath or a valid channel to launch(), or use connect() for a running browser. |
| Browser executable not found | The path is wrong in this environment, or the browser was not installed in the deployed image. | Print/check CHROME_PATH, install the binary during image build, and verify that the runtime user can execute it. |
| Browser fails to start with missing shared libraries | Linux image lacks Chrome’s required system packages. | Install the packages for the chosen distribution using the current Puppeteer system requirements; do not assume packages from another distribution match. |
| Browser launches locally but fails in CI | Different OS/architecture, missing libraries, restricted process permissions, or a mismatched browser build. | Pin the browser and Node versions, match the CI platform, install required dependencies, and inspect stderr with dumpio: true during diagnosis. |
connect() times out or reports a WebSocket error |
Endpoint is stale, unreachable, malformed, or blocked by a network boundary. | Read the current webSocketDebuggerUrl from the running browser, confirm network reachability from the Node process, and check endpoint credentials/configuration. |
| Navigation timeout | The page is slow, a resource hangs, or the chosen readiness condition waits too long. | Set a suitable timeout and wait for domcontentloaded or a selector when that meets the task. Investigate response and console errors before increasing limits. |
| Selector wait never succeeds | Selector is incorrect, content is in a frame or shadow root, or the page has not reached the state expected. | Inspect the live DOM, confirm navigation and authentication state, and target the correct frame or locator. |
| Screenshot is blank or incomplete | Capture ran before rendering, lazy assets were not loaded, or a viewport/layout assumption is wrong. | Wait for a stable page element, scroll lazy content into view where needed, set viewport before navigation, and verify the browser can access the page’s assets. |
| Browser closes after a task that should leave it running | browser.close() shuts down the browser process. |
For an externally owned browser, call browser.disconnect(). Use close() only when the script owns the browser lifetime. |
| Inconsistent output across runs | Browser drift, changing page content, shared cookies, fonts, time zone, or race conditions. | Pin the browser build, isolate contexts, set viewport and locale-related conditions deliberately, and wait for application state rather than arbitrary timing. |
10. FAQ
Does Puppeteer Core install Chrome?
No. You provide a browser executable or connect to a running browser.
Can I use Puppeteer Core with TypeScript?
Yes. It is a JavaScript package with TypeScript declarations. The current requirements list TypeScript 5.0.1+ when using TypeScript.
Can I connect to a browser running on another machine?
Yes, when its WebSocket endpoint is reachable from your Node process and the browser owner has configured access. Network security and endpoint lifecycle are your responsibility.
Should I choose Puppeteer or Puppeteer Core?
Choose puppeteer when automatic browser download and defaults suit your application. Choose puppeteer-core when you supply the browser or connect to one that is already running.
Does browser.disconnect() close Chrome?
No. It detaches Puppeteer and leaves the browser running. browser.close() closes the browser and its associated pages.
Official references: Installation, Getting started, System requirements, LaunchOptions, and ConnectOptions.


