Puppeteer: A Practical Guide to Browser Automation
Learn Puppeteer’s browser automation workflow: install it, navigate and interact with pages, capture screenshots or PDFs, and troubleshoot browser setup.
Puppeteer is a JavaScript library for controlling Chrome and Firefox. It can launch a browser, navigate pages, interact with controls, inspect content, and produce screenshots or PDFs. It uses the Chrome DevTools Protocol (CDP) or WebDriver BiDi; headless mode is the default, and you can configure a visible browser when you need one. See the official Puppeteer documentation for current APIs and examples.
This guide walks through installation, a runnable automation script, output capture, browser selection, reliability, and common setup failures. The examples use the current documented Puppeteer API style; browser compatibility and package behavior can change between releases.
1. Install Puppeteer and choose a browser setup
For the standard local setup, install puppeteer. Its install process downloads a compatible Chrome build.
npm init -y
npm install puppeteer
Use puppeteer-core when you manage the browser yourself or connect to a remote browser. It does not download a browser, so your code must specify the browser executable or connection method.
npm install puppeteer-core
| Package | Browser setup | Typical use |
|---|---|---|
puppeteer |
Downloads a compatible Chrome build during installation. | Local development and straightforward automation. |
puppeteer-core |
No browser download; you provide or connect to a browser. | Remote, container-managed, or self-managed browser environments. |
Some package managers or security settings block install scripts. If installation completed but Puppeteer cannot find Chrome, allow the package install script or run the documented browser installer:
npx puppeteer browsers install
Keep the Puppeteer and browser versions compatible. Puppeteer publishes a browser version mapping because its releases are paired with browser builds to maintain protocol compatibility. The mapping changes over time, so consult the supported browsers page for the release you install. Puppeteer has used Chrome for Testing from v20.0.0 and stable Firefox from v23.0.0, according to its documentation.
2. Launch, navigate, interact, and close
This complete Node.js script opens a page, sets its viewport, waits for a locator, reads the page title, and closes the browser even if an operation fails.
// save as automate.mjs
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1365, height: 900 });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const heading = await page.locator('h1').waitHandle();
const headingText = await heading.evaluate(element => element.textContent?.trim() ?? '');
console.log({ title: await page.title(), heading: headingText });
} finally {
await browser.close();
}
Run it with:
node automate.mjs
headless: true runs without a visible browser window. Set headless: false to observe actions while debugging in an environment with a display. In a managed browser setup using puppeteer-core, supply the appropriate executable path or connect to the managed browser using its supported connection method.
Use locators for page interaction
Locators wait for elements and provide methods for common actions. For example, to fill and submit a search field, adapt the selectors and submit action to the page being automated:
const search = page.locator('input[name="q"]');
await search.fill('Puppeteer browser automation');
await search.press('Enter');
await page.locator('main').wait();
console.log(await page.title());
Prefer selectors tied to stable attributes, labels, or accessible roles when the page offers them. Avoid depending on generated class names that can change between deployments. A locator timing out usually means the selector was wrong, the page had not reached the expected state, or the target was inside a frame that needs separate handling.
Choose navigation waits deliberately
page.goto() supports different readiness conditions. domcontentloaded waits until the initial document is parsed; load waits for the load event; networkidle0 and networkidle2 wait for network activity to fall below their respective connection thresholds. Pages with analytics, polling, or long-lived requests may never become network-idle, so use a specific selector or a suitable document event when possible.
Navigation timing is not the same as application readiness. A single-page application may render its important content after the document event. In that case, wait for the page element or state your next action actually needs.
3. Capture a screenshot or PDF
Save a screenshot
Capture a viewport screenshot with page.screenshot(). Use fullPage: true when the image should include content below the viewport.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
For a reliable capture, wait for the specific content and any required images or fonts instead of assuming that a navigation event means every visual element has settled. Full-page captures can be large and may expose lazy-loading behavior: scroll or otherwise trigger lazy content before capturing if the target page requires it.
Save a PDF
Puppeteer’s page.pdf() generates a PDF using print CSS media by default. If the page should use screen styles, emulate screen media before creating the PDF.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.pdf({ path: 'page.pdf', format: 'A4', printBackground: true });
} finally {
await browser.close();
}
For screen styling, add await page.emulateMediaType('screen') before page.pdf(). Check print-specific CSS, page breaks, background printing, and margins when the result differs from the browser view. See the PDF API reference for available options.
4. Configure browser automation for your use case
| Need | Useful Puppeteer capability | Implementation note |
|---|---|---|
| Run UI checks | Locators, keyboard input, assertions, and page events | Wait for observable page state before asserting. |
| Submit forms | Locator fill, click, and keyboard methods | Confirm validation and success states, not just the click. |
| Capture rendered pages | page.screenshot() or page.pdf() |
Set viewport and media type to match the desired output. |
| Automate Firefox | Browser selection and WebDriver BiDi | Check current supported-browser guidance and protocol support. |
| Trace performance | Tracing APIs and browser events | Keep the trace scope focused to limit output size. |
| Render SPA content | Navigation plus targeted waits and page evaluation | Wait for application content, not only the initial document. |
Other documented Puppeteer use cases include Chrome extension testing and crawling single-page applications to generate pre-rendered content. The right wait strategy and browser configuration depend on the page and runtime environment.
Browser and protocol choices
Puppeteer controls Chrome through CDP by default and also supports WebDriver BiDi. Firefox automation uses WebDriver BiDi by default. The project FAQ says Chrome automation with CDP will continue to be supported alongside BiDi. Consult the Puppeteer FAQ for current protocol guidance.
Puppeteer and Selenium
Puppeteer is a JavaScript library centered on browser control. Selenium offers more language bindings and orchestration tooling such as Selenium Grid. Both projects contribute to WebDriver BiDi. Choose based on your language requirements, browser and protocol needs, and whether you need distributed orchestration. These distinctions do not establish a universal winner for speed, reliability, or browser coverage.
5. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| “Could not find Chrome” or missing browser executable | The install script did not run, or puppeteer-core was installed without configuring a browser. |
For the standard package, run npx puppeteer browsers install or allow the install script. For core, provide a compatible managed browser. |
| Browser launches locally but not in deployment | The deployment image lacks the browser or required runtime setup, or uses a different executable path. | Install the compatible browser in the image and configure the executable or remote connection explicitly. |
| Browser closes or crashes on launch | Browser and Puppeteer versions may be mismatched, or the host environment may not support the launch configuration. | Use the browser version mapped to the installed Puppeteer release; inspect launch errors and environment requirements. |
| Navigation or locator timeout | The site is slow, an expected selector is incorrect, or the wait condition does not fit the page. | Check the URL and selector, use a targeted locator wait, and set a timeout appropriate to the environment. |
networkidle never completes |
Background requests, analytics, or polling keep the network active. | Wait for a specific selector or use a document readiness event suited to the task. |
| Screenshot omits content or differs between runs | Lazy-loaded images, animations, fonts, or delayed application rendering have not settled. | Wait for the relevant elements and assets; trigger lazy content when needed and use a consistent viewport. |
| PDF appearance differs from browser | PDF generation uses print media styles by default. | Use print CSS intentionally or call page.emulateMediaType('screen') before PDF capture. |
| Element is not found inside an iframe | The selector is being queried in the top-level page rather than the frame. | Identify the frame and query within its frame context. |
6. Performance, reliability, and cost
Browser automation starts and maintains a real browser process, so resource use depends on the page, browser, and concurrency. Reuse a browser for multiple pages in a controlled job when appropriate, and close pages and browsers when finished. Limit concurrent browser work to what the host can support; an overloaded machine can cause slow navigation, memory pressure, and flaky timeouts.
For reliable jobs, make cleanup unconditional, use explicit waits tied to the task, set navigation and action timeouts, and log the URL and operation that failed. Keep browser versions pinned or deliberately updated with their matching Puppeteer package. For repeated UI checks, isolate state such as cookies and storage when test cases must not affect one another.
Puppeteer is an open-source library; operating it still has infrastructure costs such as compute, browser storage, and maintenance. A self-managed browser gives control over the environment but makes browser installation, compatibility, and runtime upkeep your responsibility. A remote browser shifts some of that setup to its provider, with any charges depending on that provider’s terms.
Or skip the browser setup
If you need a screenshot rather than browser interaction, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Make one GET request with a URL to receive a PNG, JPEG, WebP, or PDF. Its API documentation describes the request options.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
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)
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 Bun.write('shot.webp', res);
ScreenshotNeo accepts cookie and consent banners as a visitor would, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots; all features are on every plan, and yearly billing gives two months free. For a one-off screenshot or capture workflow, this can avoid installing and maintaining a browser. Puppeteer remains the fit when your task needs custom browser interaction or test logic.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
Frequently asked questions
Does Puppeteer support WebDriver BiDi?
Yes. Puppeteer supports WebDriver BiDi as well as CDP; Chrome uses CDP by default, while Firefox uses BiDi by default in the documented setup.
Will Puppeteer keep supporting CDP?
The project FAQ says it will continue supporting Chrome automation with CDP alongside WebDriver BiDi.
Is Puppeteer a replacement for Selenium?
That depends on your project. Puppeteer fits JavaScript browser control; Selenium supports more language bindings and includes orchestration tooling such as Selenium Grid.
Why does my Puppeteer version not work with a particular Chrome or Firefox version?
Puppeteer releases are paired with browser versions to maintain protocol compatibility. Check the supported-browser mapping for the release you use.
Can Puppeteer run without a visible browser?
Yes. Headless mode is the default. Configure headful mode when you need to see the browser and have a display environment available.


