View all services
Talk to QA Advisor
Browse the Knowledge Hub211 resources
/Tools Library/Puppeteer
Tools Library · Review and guide

Puppeteer review: what it is, tutorial and alternatives

Puppeteer is a JavaScript library maintained by Google’s Chrome Browser Automation team for controlling Chrome or Firefox over the Chrome DevTools Protocol or WebDriver BiDi. It is a library rather than a test framework, with no runner, assertions or reports. It suits scripted automation such as screenshots, PDFs, crawling and performance traces, and can back a test suite when paired with a runner.

QAble has evaluated this tool and works alongside it. The factual sections are complete; our view is bounded to what we have actually seen rather than a long-term verdict.

last reviewed
13 October 2026
version covered
Puppeteer 25
licence
Open source

Reviewed by Viral Patel

Co-Founder, QAbleLast reviewed 13 October 2026

At a glance

Puppeteer at a glance

Official site
Vendor
Google
Category
Web automation
Licence
Open source (Apache 2.0)
Platforms
Chrome, Firefox
Languages
JavaScript, TypeScript
First released
2017
Version reviewed
Puppeteer 25

Verdict

Our verdict on Puppeteer

Strengths

  • Direct Chrome DevTools Protocol access for tracing, emulation, coverage and other Chrome-specific capabilities
  • Downloads a tested Chrome for Testing build, so library and browser versions always match
  • Locators wait for elements to be visible, enabled and stable before acting, and retry the action
  • page.pdf and page.screenshot turn any page into a PDF or image in a few lines
  • Firefox is supported through WebDriver BiDi, and Chrome can be driven over BiDi too
  • Free under Apache 2.0, maintained by Google’s Chrome Browser Automation team

Weaknesses

  • No test runner, assertions, retries or reports: testing needs node:test, Jest, Mocha or similar alongside it
  • JavaScript and TypeScript only, with no WebKit or Safari support
  • Version 25 is ES modules only and needs Node.js 22.12 or later
  • Several features are CDP-only and throw UnsupportedOperation over WebDriver BiDi
  • Package managers that block install scripts skip the browser download, so first runs fail

Our take

Puppeteer earns its place in browser automation that is not primarily testing: generating PDFs and screenshots, pre-rendering pages, crawling and capturing performance traces, where a small library with direct DevTools Protocol access is exactly what the job needs. For an end-to-end test suite it is the wrong starting point. Playwright Test supplies the runner, retrying assertions, parallelism, tracing, reporting and WebKit coverage that Puppeteer leaves you to assemble. QAble has evaluated Puppeteer rather than maintained a long-running suite on it, so treat this as a bounded take. If you already depend on it, the current major, version 25 from 12 May 2026, ships as ES modules only and needs Node.js 22.12 or later, so check your module setup and build tooling before upgrading.

We have evaluated this tool rather than maintained a long-running suite on it, so this is a bounded view rather than a long-term verdict. The rest of this page is factual.

How it works

What is Puppeteer and how does it work?

Two protocols, chosen per browser

Puppeteer speaks two protocols. For Chrome it uses the Chrome DevTools Protocol by default, the same low-level interface Chrome’s developer tools use, which exposes almost everything the browser can do. For Firefox it uses WebDriver BiDi, the W3C’s bidirectional standard, which became production-ready in Puppeteer 23; Firefox over CDP was removed in version 24. You can also launch Chrome with protocol set to webDriverBiDi. The two are not equivalent: features that have no BiDi counterpart, including several emulation methods and CDP-specific request details, throw an UnsupportedOperation error over BiDi, so a script written against Chrome may need changes before it runs on Firefox.

One pinned browser per release

Every Puppeteer release is tied to a specific Chrome and Firefox release so protocol changes cannot break it unexpectedly. Installing the puppeteer package downloads that Chrome for Testing build, plus chrome-headless-shell, into ~/.cache/puppeteer. The puppeteer-core package downloads nothing and reads no configuration files: it is for connecting to a remote browser or launching one you manage, with an explicit executablePath or channel. Configuration lives in a .puppeteerrc file or environment variables such as PUPPETEER_CACHE_DIR, and changes to download settings only take effect after you run the browser install again.

Locators wait; handles do not

page.locator is the recommended way to act on elements. Before a click or fill, a locator scrolls the element into view, waits for it to be visible and enabled, and waits for its bounding box to stay the same over two animation frames, then retries the action if it fails, up to the page’s timeout. The older APIs behave differently. waitForSelector waits for an element and returns a handle, but does not retry the action and the handle must be disposed. page.$ and page.$$ query immediately without waiting. page.click with a selector waits for the element to exist but does not apply the locator checks, so mixing styles produces inconsistent synchronisation.

Headless by default, in two flavours

Puppeteer launches headless by default, and since version 22 that means Chrome’s new headless mode, which runs the same browser as a headed session without a window. The old headless implementation now ships as a separate binary, chrome-headless-shell, selected with headless set to shell. It is faster for jobs that do not need the full feature set, such as bulk screenshots, but does not match Chrome’s behaviour exactly, so test-like work should stay on the default. Setting headless to false opens a visible window for debugging, which on a Linux CI machine needs a virtual display such as Xvfb.

Strengths

What it does well

PDF and screenshot generation

page.pdf renders the page with Chrome’s print pipeline, with options for paper format, margins, headers and footers, and background graphics; accessible PDFs are the default since version 22. page.screenshot captures the viewport, the full page or one element. These two calls are behind a large share of Puppeteer’s use in production services.

Request interception

With setRequestInterception enabled, every request pauses until a handler continues it, responds with a stub or aborts it. That makes it possible to block third-party scripts, serve fixtures for an API or simulate failures. Handlers should check isInterceptResolutionHandled first, because several handlers, including ones from plugins, may see the same request.

Selectors beyond CSS

Puppeteer extends CSS with prefixed pseudo-elements: ::-p-aria finds elements by accessible name and role, ::-p-text by visible text, and ::-p-xpath by XPath. The >>> combinator descends into open shadow roots. ARIA selectors are the closest Puppeteer gets to Playwright’s role locators, and they survive markup changes better than class names.

Performance and DevTools data

Because it speaks CDP natively, Puppeteer can record a Chrome performance timeline with page.tracing for analysis in DevTools, throttle the network with PredefinedNetworkConditions, throttle the CPU and emulate devices from KnownDevices. For performance investigations scripted in Chrome, this direct access is the main reason to choose it over a test framework.

Chrome extension testing

The documentation includes a guide to testing Chrome extensions: loading them at launch or at runtime, reaching the extension’s service worker or background page, and opening its popup. Extension testing depends on Chrome-specific behaviour, so it is a case where a CDP-first library is a natural fit.

An official Docker image

Puppeteer publishes a Docker image on the GitHub Container Registry with Chrome for Testing, its system dependencies and a matching Puppeteer version, tagged per release. It runs Chrome with its sandbox enabled, so the container needs the SYS_ADMIN capability, and an init process such as docker run --init so browser processes are cleaned up.

Pricing

Puppeteer pricing and licensing

Puppeteer is free and open source under the Apache 2.0 licence, maintained by Google’s Chrome Browser Automation team, with no paid edition, hosted service or feature tiers. Costs come from where it runs: CI minutes, servers or serverless functions for PDF and screenshot workloads, and the engineering time to build the runner, reporting and parallelism that Puppeteer deliberately leaves out.

  • Licence: Apache 2.0, stated in the puppeteer/puppeteer repository and on npm for both puppeteer and puppeteer-core.
  • No tiers: the whole library is free, with no account, key or usage limit.
  • Infrastructure: each browser instance needs real memory and CPU, so a production PDF or screenshot service is sized by how many browsers run at once.
  • Testing overhead: a runner, assertions, retries, reports and parallelism come from other packages or your own code, which is engineering time Playwright Test would not need.

Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.

Getting started

Puppeteer tutorial: getting started

Setup, step by step

  1. Install Node.js 22.12 or later, the minimum for Puppeteer 25.
  2. Create a project and set "type": "module" in package.json, since Puppeteer 25 ships as ES modules.
  3. Run npm i puppeteer, which downloads a matching Chrome for Testing build.
  4. If your package manager blocked install scripts, run npx puppeteer browsers install.
  5. Save the example as login.test.js and run node --test.

Install

npm i puppeteer
# if install scripts were blocked, download the pinned browser explicitly
npx puppeteer browsers install
node --test

A first example

// login.test.js (package.json has "type": "module")
import { test, before, after } from 'node:test'
import assert from 'node:assert/strict'
import puppeteer from 'puppeteer'

let browser
before(async () => {
  browser = await puppeteer.launch() // headless Chrome for Testing
})
after(async () => {
  await browser.close()
})

test('a valid user can sign in', async () => {
  const page = await browser.newPage()
  await page.goto('https://the-internet.herokuapp.com/login')

  // Locators wait for the element to be visible, enabled and stable
  await page.locator('#username').fill('tomsmith')
  await page.locator('#password').fill('SuperSecretPassword!')
  await Promise.all([
    page.waitForNavigation(),
    page.locator('button[type=submit]').click(),
  ])

  // No assertion library: read the value, then assert with node:assert
  const message = await page.locator('#flash').map(el => el.textContent).wait()
  assert.match(message, /You logged into a secure area!/)
  await page.close()
})

What trips people up first

The browser that never arrived. npm, pnpm, Yarn Berry, Bun and Deno can all block dependency install scripts, and when they do, Puppeteer’s download of Chrome is skipped and the first launch fails with Could not find Chrome. Run npx puppeteer browsers install, or allow the script for puppeteer in your package manager. The second hurdle is modules: Puppeteer 25 is published as ES modules only, so use import and an ES module project. On Ubuntu 23.10 or later, a third is the No usable sandbox error, caused by an AppArmor policy; follow the troubleshooting guide rather than reaching for --no-sandbox.

In practice

Using Puppeteer in practice

A small test harness with node:test

Puppeteer has no runner, so a test suite starts by choosing one. Node’s built-in test runner needs no extra dependency, runs each test file in its own process with several files at once by default, and reports in spec, TAP or JUnit formats. Put browser lifecycle in one helper: launch a browser per file, give every test a fresh browser context so cookies and storage never leak between tests, and close the context afterwards. Set a default timeout per page so a missing element fails in seconds rather than hanging. Organise files by feature and keep reusable page actions in small modules, much as page objects work elsewhere.

// test/helpers.js
import { before, after, beforeEach, afterEach } from 'node:test'
import puppeteer from 'puppeteer'

export function useBrowser() {
  const state = {}
  before(async () => {
    state.browser = await puppeteer.launch()
  })
  beforeEach(async () => {
    // A fresh browser context per test: separate cookies and storage
    state.context = await state.browser.createBrowserContext()
    state.page = await state.context.newPage()
    state.page.setDefaultTimeout(10_000)
  })
  afterEach(async () => {
    await state.context.close()
  })
  after(async () => {
    await state.browser.close()
  })
  return state
}

// test/orders.test.js
import { test } from 'node:test'
import { useBrowser } from './helpers.js'

const ctx = useBrowser()

test('lists existing orders', async () => {
  await ctx.page.goto('http://localhost:3000/orders')
  // wait() throws a TimeoutError if no row becomes visible
  await ctx.page.locator('[data-test=order-row]').wait()
})

Stubbing an API with request interception

Request interception turns the network into something a script controls. Enable it before navigating, then decide for every request: respond with a stub, continue it unchanged, or abort it. Every request stalls until one of those happens, so a handler that forgets to continue unmatched requests hangs the page. Check isInterceptResolutionHandled before acting, as the documentation advises, because another handler may already have resolved the request. For assertions about what appears, wait on a locator; wait() fails with a timeout if the element never becomes visible, which is a usable assertion in its own right.

// test/orders-error.test.js
import { test } from 'node:test'
import { useBrowser } from './helpers.js'

const ctx = useBrowser()

test('offers a retry when the orders API fails', async () => {
  const { page } = ctx
  await page.setRequestInterception(true)
  page.on('request', request => {
    // Another handler may already have resolved this request
    if (request.isInterceptResolutionHandled()) return
    if (request.url().endsWith('/api/orders')) {
      request.respond({
        status: 500,
        contentType: 'application/json',
        body: JSON.stringify({ message: 'down' }),
      })
    } else {
      request.continue() // every other request must still be released
    }
  })
  await page.goto('http://localhost:3000/orders')
  await page.locator('::-p-aria([name="Try again"][role="button"])').wait()
})

Generating PDFs and screenshots

For document generation, render the page your application already serves rather than building PDFs by hand. Wait for the network to go quiet so fonts and images have loaded, set printBackground if the design relies on background colours, and pick a paper format. Close the browser in a finally block: a script that throws before closing leaves Chrome processes running, and a long-running service accumulates them until the machine runs out of memory. For a service handling many requests, reuse one browser and open a page or context per job rather than launching Chrome every time.

// render-invoice.js
import puppeteer from 'puppeteer'

const browser = await puppeteer.launch()
try {
  const page = await browser.newPage()
  await page.goto('http://localhost:3000/invoices/42', { waitUntil: 'networkidle0' })
  await page.pdf({ path: 'invoice-42.pdf', format: 'A4', printBackground: true })
  await page.screenshot({ path: 'invoice-42.png', fullPage: true })
} finally {
  await browser.close() // always close, or Chrome processes pile up
}

Running Puppeteer in GitHub Actions

A Puppeteer job is an ordinary Node.js job with two additions. Download the pinned browser explicitly, because CI package managers increasingly block install scripts. And on Ubuntu runners, apply the AppArmor workaround from the Chromium documentation that Puppeteer’s troubleshooting guide links to, since Ubuntu 23.10 and later can stop Chrome for Testing from creating its sandbox. Both steps are cheap and make the job independent of runner image changes. Node’s test runner can write JUnit XML alongside the console output for the CI summary. Start your application in an earlier step if the tests need it.

# .github/workflows/puppeteer.yml
name: puppeteer
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      # Install scripts may be blocked, so fetch the pinned Chrome explicitly
      - run: npx puppeteer browsers install
      # Ubuntu 23.10+ AppArmor can block Chrome for Testing's sandbox
      - run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
      - run: >-
          node --test
          --test-reporter=spec --test-reporter-destination=stdout
          --test-reporter=junit --test-reporter-destination=junit.xml
      - uses: actions/upload-artifact@v7
        if: ${{ !cancelled() }}
        with:
          name: puppeteer-results
          path: junit.xml

Upgrading to Puppeteer 25

Version 25, released on 12 May 2026, mostly removes things that were already deprecated, but it changes how the package loads. It is ES modules only, so CommonJS code and build tools that expect CommonJS need attention. Node.js 22.12 is the minimum. puppeteer.executablePath() and defaultArgs() now return promises, so add await. Browser.isConnected() is gone in favour of the connected property, the clickCount mouse option is replaced by count, and Puppeteer.product is removed. Older removals still catch people copying tutorials: version 22 removed waitForTimeout, $x and waitForXPath and renamed createIncognitoBrowserContext, and version 23 renamed the product launch option to browser and the option for ignoring HTTPS errors to acceptInsecureCerts.

Pitfalls

Common pitfalls

Could not find Chrome on the first run

Why it happens: Package managers that block dependency install scripts skip Puppeteer’s postinstall step, so the browser download never happens. A changed home directory in CI or Docker has the same effect.

The fix: Run npx puppeteer browsers install after installing dependencies, or allow the puppeteer install script. Set PUPPETEER_CACHE_DIR when the home directory differs between install and run.

Copying waitForTimeout, $x and other removed APIs

Why it happens: Many tutorials predate version 22, which removed page.waitForTimeout, $x and waitForXPath, and renamed createIncognitoBrowserContext to createBrowserContext.

The fix: Wait on a locator or a condition instead of a delay, use ::-p-xpath selectors with locators, and create isolated sessions with browser.createBrowserContext.

Loading Puppeteer 25 from CommonJS

Why it happens: Puppeteer 25 moved to ES modules only. CommonJS code, older bundlers and test runners that transform code to CommonJS may fail to load it.

The fix: Switch the project to ES modules and use import, or check that your tooling supports loading ES modules before upgrading. Stay on 24 until it does.

Request handlers that hang the page

Why it happens: With interception enabled, every request waits until it is continued, answered or aborted. A handler that only deals with the requests it cares about leaves the rest stalled.

The fix: End every handler with a branch that continues unmatched requests, check isInterceptResolutionHandled first, and disable interception when you no longer need it.

Leaking Chrome processes

Why it happens: A script that throws before browser.close leaves Chrome running. In a service or a long test run, orphaned processes accumulate until memory runs out.

The fix: Close in a finally block or an after hook, run containers with an init process, and reuse one browser with a context per job rather than launching repeatedly.

Disabling the sandbox to make Linux CI pass

Why it happens: No usable sandbox errors on Ubuntu 23.10 and later come from an AppArmor policy, and --no-sandbox makes them disappear, along with Chrome’s main protection against hostile pages.

The fix: Apply one of the documented workarounds, such as relaxing the user namespace restriction on the CI host or using the official Docker image with SYS_ADMIN.

Compared

Puppeteer alternatives compared

ToolLanguagesBrowsersProtocolTest runnerAuto-waitingNetwork interception
PuppeteerJavaScript, TypeScriptChrome, FirefoxCDP or WebDriver BiDiNone; bring your ownLocators onlyYes, setRequestInterception
PlaywrightJS, TS, Python, Java, C#Chromium, Chrome, Edge, Firefox, WebKitCDP for Chromium; patched Firefox, WebKitPlaywright Test, built inYes, every actionYes, page.route
SeleniumJava, Python, C#, Ruby, JSChrome, Edge, Firefox, SafariWebDriver Classic and BiDiNone; uses JUnit, pytest and othersNo, explicit waitsBiDi, or CDP on Chromium
WebdriverIOJavaScript, TypeScriptChrome, Edge, Firefox, SafariWebDriver BiDi or Classicwdio testrunner, built inYes, for actions and matchersBuilt in, needs BiDi

Head-to-head comparisons

Puppeteer vs Playwright

FAQ

Frequently asked questions

What is Puppeteer?

Puppeteer is a JavaScript library maintained by Google’s Chrome Browser Automation team for controlling Chrome or Firefox over the Chrome DevTools Protocol or WebDriver BiDi. It is a library rather than a test framework, with no runner, assertions or reports. It suits scripted automation such as screenshots, PDFs, crawling and performance traces, and can back a test suite when paired with a runner.

What are the best Puppeteer alternatives?

The alternatives most often weighed against Puppeteer are Playwright, Selenium and WebdriverIO. The comparison on this page sets them side by side on languages, browsers, protocol, test runner, auto-waiting and network interception, and the verdict above says where Puppeteer is the wrong choice and which of them to pick instead.

Is Puppeteer free?

Yes. Puppeteer is open source under the Apache 2.0 licence and maintained by Google’s Chrome Browser Automation team. There is no paid edition, hosted service or licence key. Your costs are the machines that run the browsers, which matters for PDF and screenshot services, and the time to add the test runner and reporting Puppeteer does not include.

Should I use Puppeteer or Playwright?

For end-to-end testing, Playwright: it adds a test runner, retrying assertions, parallel workers, tracing, reports, WebKit and four more languages. Puppeteer is a sound choice for Chrome-centred scripting, such as PDFs, screenshots, crawling, extension testing and performance traces, where its direct DevTools Protocol access and small surface are an advantage and a test runner would be unnecessary weight.

Can Puppeteer be used for testing?

Yes, with a runner. The Puppeteer FAQ itself points to community projects such as jest-puppeteer for test conveniences. Node’s built-in node:test works well and needs no extra dependency. You supply assertions, per-test isolation through browser contexts, screenshots on failure and reporting. For a large suite, that assembly work is the main argument for a framework instead.

Does Puppeteer support Firefox and Safari?

Firefox, yes: since version 23 Puppeteer drives Firefox through WebDriver BiDi, and it can download a matching Firefox build. Some features that depend on the Chrome DevTools Protocol, such as several emulation methods, throw UnsupportedOperation there. Safari and WebKit are not supported. For Safari, use Selenium or WebdriverIO with safaridriver, or Playwright’s WebKit build as an approximation.

Why does Puppeteer say it could not find Chrome?

Usually because the browser download never ran. Puppeteer fetches Chrome for Testing in an install script, and npm, pnpm, Yarn Berry, Bun and Deno can block those scripts. Run npx puppeteer browsers install, or allow the puppeteer script. If the home directory differs between install and run, as in some CI and Docker setups, set PUPPETEER_CACHE_DIR to a fixed path for both.

What changed in Puppeteer 25?

Puppeteer 25 was released on 12 May 2026. It ships as ES modules only, requires Node.js 22.12 or later, and makes executablePath and defaultArgs return promises. It removes deprecated APIs including Browser.isConnected, the clickCount mouse option and Puppeteer.product. Releases within version 25 continue to roll Chrome and Firefox forward; 25.13.0 on 8 October 2026 moved to Chrome 155 and Firefox 157.

Is Puppeteer still maintained?

Yes. Google’s Chrome Browser Automation team maintains it, with releases every few weeks that roll the pinned Chrome and Firefox versions forward, and the FAQ states that CDP support for Chrome will continue alongside WebDriver BiDi. The project describes itself as a reference implementation of both protocols, and its documentation now covers MCP and WebMCP alongside classic scripting.

Alternatives

Puppeteer alternatives worth weighing

Resources

Where to go next

Ready

Picking the wrong tool costs more than picking late.

QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.

Talk to QA Advisor