---
title: "Playwright vs Puppeteer for Web Scraping | Scrape.ly"
description: "Playwright vs Puppeteer for web scraping: auto-waiting, locators, network interception and tracing, with the same Node.js scraper in both, tested."
url: https://scrape.ly/blog/playwright-vs-puppeteer
markdown: https://scrape.ly/blog/playwright-vs-puppeteer.md
---

[Guides](https://scrape.ly/blog/category/guides.md)

# Playwright vs Puppeteer for Web Scraping: The Same Scraper in Both (Node.js)

Playwright vs Puppeteer for web scraping: languages, browsers, auto-waiting, locators, network interception, tracing and stealth plugins, checked on the official docs on 23 October 2026, plus the same Node.js scraper written in both and tested.

[Scrape.ly Team The team behind Scrape.ly](https://scrape.ly/blog/author/scrapely-team.md)

- Published

  Oct 23, 2026

- Reading time

  10 min read

![Pixel-art robot with an orange >\_ on its terminal head stands thinking between two matching browser windows on stands, one with the Playwright logo and one with the green Puppeteer logo, next to a crate bearing the Scrape.ly logo](https://scrape.ly/media/playwright-vs-puppeteer.png?w=1080)

Playwright vs Puppeteer for web scraping: Playwright is the better default, because it works in four languages, waits for elements before every action, and ships a trace viewer and a code generator that make a broken scraper quick to fix. Puppeteer is a lean JavaScript library for Chrome and Firefox, and it stays a good choice when your scraper is already Node.js and Chrome-only.

Below is how they compare on what matters for scraping, using facts from playwright.dev and pptr.dev as of 23 October 2026, then the same scraper written in both and run against the same JavaScript-rendered page. At the end: how to run either one on a hosted browser built for scraping.

## Playwright vs Puppeteer at a glance

|                                        | Playwright                                                                    | Puppeteer                                                                     |
| -------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Latest version on npm (23 Oct 2026)    | 1.64.1, released 14 October 2026                                              | 25.15.0, released 20 October 2026                                             |
| Languages                              | TypeScript/JavaScript, Python, .NET, Java                                     | JavaScript/TypeScript                                                         |
| Browsers                               | Its own Chromium, Firefox and WebKit builds, plus installed Chrome and Edge   | Chrome for Testing and stable Firefox                                         |
| Auto-waiting                           | Before every action                                                           | In the Locator API; `page.$()` and `$$eval()` don't wait                      |
| Selectors                              | Locators, including `getByRole`, `getByLabel`, `getByText`                    | CSS, plus `::-p-xpath()`, `::-p-text()`, `::-p-aria()`                        |
| Network interception                   | `page.route()` / `context.route()`: abort, modify, mock, or rewrite responses | `setRequestInterception(true)`: every request waits for your handler          |
| Proxy per session                      | Per browser context                                                           | Per browser context (`proxyServer`)                                           |
| Debugging                              | Trace Viewer (DOM snapshots, network, console per action) and `codegen`       | Chrome performance traces; Chrome DevTools Recorder exports Puppeteer scripts |
| Stealth plugin                         | Via `playwright-extra`                                                        | Via `puppeteer-extra` (the plugin's original home)                            |
| `navigator.webdriver` in headless mode | `true` (our test)                                                             | `true` (our test)                                                             |
| npm downloads, 12–18 Oct 2026          | 69.8 million                                                                  | 6.7 million                                                                   |

## Languages and browsers

Playwright is "available for TypeScript, Python, .NET, and Java" and drives Chromium, Firefox and WebKit with one API ([playwright.dev](https://playwright.dev/)). `npx playwright install chromium` downloads the browser build that matches your Playwright version, and the `channel` option lets it drive the Google Chrome or Microsoft Edge installed on your machine ([browsers docs](https://playwright.dev/docs/browsers)).

Puppeteer describes itself as "a JavaScript library which provides a high-level API to control Chrome or Firefox over the DevTools Protocol or WebDriver BiDi" ([pptr.dev](https://pptr.dev/)). Since version 20 it works with Chrome for Testing, and since version 23 with stable Firefox ([supported browsers](https://pptr.dev/supported-browsers)). `npm i puppeteer` downloads a compatible Chrome during installation. The docs warn that package managers which block install scripts skip that download; `npx puppeteer browsers install` fetches it afterwards. In our test, installing Puppeteer 25.15.0 with `npm i puppeteer` downloaded Chrome for Testing 155.0.8059.84 on the first try.

For scraping, the browser list rarely matters, because nearly every scraper runs Chromium. The language list matters more: if your team writes Python, Java or C#, the choice is made. Puppeteer has no official version for them.

## The same scraper in both

The task: scrape the first three pages of [quotes.toscrape.com/js](https://quotes.toscrape.com/js/), a sandbox site whose quotes are written into the page by JavaScript, so a plain HTTP request sees none of them. Both scripts block images, fonts and stylesheets, and paginate by clicking the **Next** link. Install both libraries with `npm i playwright puppeteer`, run `npx playwright install chromium`, and save each script as an `.mjs` file.

**Playwright:**

```javascript
import { chromium } from 'playwright';

const BLOCKED = new Set(['image', 'font', 'stylesheet']);

const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();

// Skip images, fonts and stylesheets: the quotes are in the HTML.
await page.route('**/*', (route) =>
  BLOCKED.has(route.request().resourceType()) ? route.abort() : route.continue(),
);

await page.goto('https://quotes.toscrape.com/js/');

const quotes = [];
for (let pageNo = 1; pageNo <= 3; pageNo++) {
  const cards = page.locator('div.quote');
  await cards.first().waitFor(); // the quotes are written by JavaScript
  for (const card of await cards.all()) {
    quotes.push({
      text: await card.locator('span.text').innerText(),
      author: await card.locator('small.author').innerText(),
      tags: await card.locator('a.tag').allInnerTexts(),
    });
  }
  const next = page.locator('li.next > a');
  if ((await next.count()) === 0) break;
  await next.click(); // waits until the link is visible, stable and enabled
  await page.waitForURL(`**/js/page/${pageNo + 1}/`);
}

console.log(quotes.length, quotes[0]);
await browser.close();
```

**Puppeteer:**

```javascript
import puppeteer from 'puppeteer';

const BLOCKED = new Set(['image', 'font', 'stylesheet']);

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();

// Skip images, fonts and stylesheets: the quotes are in the HTML.
await page.setRequestInterception(true);
page.on('request', (request) => {
  if (request.isInterceptResolutionHandled()) return;
  if (BLOCKED.has(request.resourceType())) request.abort();
  else request.continue();
});

await page.goto('https://quotes.toscrape.com/js/');

const quotes = [];
for (let pageNo = 1; pageNo <= 3; pageNo++) {
  await page.waitForSelector('div.quote'); // the quotes are written by JavaScript
  const rows = await page.$$eval('div.quote', (cards) =>
    cards.map((card) => ({
      text: card.querySelector('span.text').innerText,
      author: card.querySelector('small.author').innerText,
      tags: [...card.querySelectorAll('a.tag')].map((tag) => tag.innerText),
    })),
  );
  quotes.push(...rows);
  const next = await page.$('li.next > a');
  if (!next) break;
  await Promise.all([page.waitForNavigation(), next.click()]);
}

console.log(quotes.length, quotes[0]);
await browser.close();
```

We ran both on 23 October 2026 on macOS (Apple silicon) with Node.js 23.9.0, Playwright 1.64.1 (Chromium 156.0.8078.4) and Puppeteer 25.15.0 (Chrome for Testing 155.0.8059.84). Both collected 30 different quotes and printed the same first record:

```plaintext
30 {
  text: '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”',
  author: 'Albert Einstein',
  tags: [ 'change', 'deep-thoughts', 'thinking', 'world' ]
}
```

The scripts are almost the same length. Three differences show up anyway:

- **Pagination.** In Puppeteer you start waiting for the navigation before you click, with `Promise.all`. In Playwright you click, then wait for the URL you expect.
- **Extraction.** Puppeteer's `$$eval()` runs one function inside the page and returns plain objects. Playwright reads each field through a locator; its `evaluateAll()` does the same thing as `$$eval()` if you prefer that style.
- **Request blocking.** Playwright's route handler only runs for the URLs you match. Puppeteer's interception holds every request until a handler decides, which is why the docs recommend the `isInterceptResolutionHandled()` check.

## Auto-waiting and locators

Most flaky scrapers fail on timing: the script looks for an element a moment before the page has drawn it. Playwright runs actionability checks before every action. A click waits until the element is visible, stable, enabled and receiving events, and fails with a `TimeoutError` only if that doesn't happen in time ([auto-waiting docs](https://playwright.dev/docs/actionability)). Its locators can also find elements the way a person sees them, with `getByRole`, `getByLabel`, `getByPlaceholder` and `getByTestId`.

Puppeteer added the same idea with its Locator API, which its docs call "the recommended way to select an element and interact with it". Before a click, a locator makes sure the element is in the viewport, visible, enabled and has "a stable bounding box over two consecutive animation frames" ([page interactions](https://pptr.dev/guides/page-interactions)). The older calls are still there and are what the scraper above uses: `page.$()`, `page.$$()` and `$$eval()` query "without waiting", and `waitForSelector` "does not automatically retry the action if it fails". Puppeteer's selectors are CSS by default, extended with `::-p-xpath()`, `::-p-text()` and `::-p-aria()` and the `>>>` combinator for shadow DOM.

So both have auto-waiting today. In Playwright it is the whole API; in Puppeteer it is one API next to older ones you need to avoid mixing up.

## Network interception

Blocking heavy resources saves bandwidth on every page, which adds up when you pay per GB of proxy traffic. Capturing the JSON a page fetches is often cleaner than parsing its HTML.

Playwright's `page.route()` and `context.route()` can abort a request, continue it with changed headers or method, answer it with your own data, or fetch the real response and modify it ([network docs](https://playwright.dev/docs/network)). `page.waitForResponse()` grabs an API response triggered by a click. Proxies can be set for the whole browser or per context.

In Puppeteer, `page.setRequestInterception(true)` turns interception on, and from then on "every request will stall unless it's continued, responded or aborted" ([request interception](https://pptr.dev/guides/network-interception)). If two handlers resolve the same request, Puppeteer throws `Request is already handled!`, so the docs describe a Cooperative Intercept Mode with priorities for code that registers several handlers. A proxy per context works through `browser.createBrowserContext({ proxyServer })`, with credentials set via `page.authenticate` ([context options](https://pptr.dev/api/puppeteer.browsercontextoptions)).

Both can do what a scraper needs. Playwright's version is simpler to combine with other code, because routes only touch the URLs they match.

## Debugging: Trace Viewer and codegen

This is where Playwright pulls clearly ahead. When a scraper fails on page 47 at 3 a.m., you want to see what the page looked like. Playwright's Trace Viewer shows every action with DOM snapshots before and after it, the network requests, the console logs and a screenshot film strip ([Trace Viewer docs](https://playwright.dev/docs/trace-viewer)). In a plain script, wrap the work in a trace:

```javascript
const context = await browser.newContext();
await context.tracing.start({ screenshots: true, snapshots: true });
const page = await context.newPage();
// ... scrape ...
await context.tracing.stop({ path: 'trace.zip' });
```

Open it with `npx playwright show-trace trace.zip`. In our test, a two-step run produced a 145 KB trace file. To write a first draft of a scraper, `npx playwright codegen https://quotes.toscrape.com/js/` records your clicks as code and picks locators for the elements you point at ([codegen docs](https://playwright.dev/docs/codegen)).

Puppeteer's `page.tracing.start()` writes a performance trace "which can be opened in Chrome DevTools or timeline viewer" ([Tracing API](https://pptr.dev/api/puppeteer.tracing)). That's a performance profile, not a step-by-step replay. For recording, Chrome's DevTools Recorder panel can export a recorded flow as a Puppeteer script ([Recorder reference](https://developer.chrome.com/docs/devtools/recorder/reference)).

## Stealth and detection

Neither library hides that it is automated. In our test, `navigator.webdriver` was `true` in headless Playwright Chromium and in headless Puppeteer Chrome for Testing, a flag any script on the page can read.

The usual fix is the same plugin for both: `puppeteer-extra-plugin-stealth` 2.11.2, used through `puppeteer-extra` 3.3.6 in Puppeteer or `playwright-extra` 4.3.6 in Playwright. The plugin started in the Puppeteer world, and none of the three packages has had a release since March 2023. We tested the setup in both libraries, with results, in our [Playwright stealth guide](https://scrape.ly/blog/playwright-stealth.md). The short version: stealth patches the browser's JavaScript surface, not your IP address, your TLS handshake or how your scraper behaves, so it doesn't decide whether you get past serious bot protection.

## Is Playwright or Puppeteer faster?

We timed the two scripts above, alternating them five times each on the same machine and network, measuring the whole script from launch to close. Playwright took 2.00 to 2.42 seconds (median 2.04), Puppeteer 2.18 to 2.47 seconds (median 2.30). Most of that time is waiting on the network, and the gap is about the size of the run-to-run noise, so we don't call either one faster. For scraping, speed comes from blocking resources you don't need, running pages in parallel and skipping the browser when a page doesn't need one, not from the library.

## Puppeteer vs Playwright: which should you pick for scraping?

- **Starting a new scraper:** Playwright. Auto-waiting everywhere, simple request routing, and the Trace Viewer for when something breaks.
- **Your team writes Python, Java or C#:** Playwright, since Puppeteer is JavaScript-only.
- **You have a working Puppeteer scraper:** keep it, and use the Locator API for new code. Rewrite only when debugging starts costing you time.
- **You want the smallest Chrome-only dependency in Node.js:** Puppeteer is a fine fit.
- **The site blocks headless browsers:** neither library alone. You need real fingerprints and good IPs, covered next.

If you're also weighing Selenium, our [Playwright vs Selenium](https://scrape.ly/blog/playwright-vs-selenium.md) comparison runs the same quotes scraper in Python with both.

## Run Playwright or Puppeteer on the Scrape.ly browser

Picking the library is the easy part. Keeping a browser scraper alive means proxies, fingerprints, challenge pages, retries and scheduling. On Scrape.ly you push your scraper as an Actor and let the platform handle that, whichever library you chose.

The [Scrape.ly browser](https://scrape.ly/docs/browser.md) is a hosted Chromium with anti-bot fingerprinting, Cloudflare Turnstile solving and its own residential proxy. It's off by default. You turn it on per Actor under **Settings** → **Scrapely Browser** and rebuild. From then on, every `chromium.launch()` in Playwright and every `puppeteer.launch()` connects to it over CDP without code changes, and so do Crawlee's `PlaywrightCrawler` and `PuppeteerCrawler`. Firefox and WebKit launches are redirected to it too.

The browser doesn't take a proxy URL. You pick where its traffic exits, and which operating system the fingerprint shows, with a browser configuration:

```javascript
import { Actor, PlaywrightCrawler } from 'scrapely';

await Actor.main(async () => {
    const browserConfiguration = await Actor.createBrowserConfiguration({
        countryCode: 'US',
        state: 'California',
        os: 'windows',
    });

    const crawler = new PlaywrightCrawler({
        browserConfiguration,
        async requestHandler({ page }) {
            await Actor.pushData({ title: await page.title() });
        },
    });
    await crawler.run(['https://example.com']);
});
```

Time zone and language follow the exit country unless you set them, so the fingerprint matches the IP, which is exactly what stealth plugins can't do on their own. You can choose Windows, macOS or Android, and a city or state. Browser crawlers such as `PuppeteerCrawler` take the same `browserConfiguration` option. It costs $0.45 per browser hour at list price, billed in 30-second blocks plus the browser's proxy traffic, and it counts toward the run's maximum charge, so a run can't spend more than the cap you set.

To start from working code, use the [Crawlee + Playwright template](https://scrape.ly/templates/crawlee-playwright.md) or the [Crawlee + Puppeteer template](https://scrape.ly/templates/crawlee-puppeteer-chrome.md): `scrapely create my-actor --template crawlee-puppeteer-chrome`, put your parsing in the router, and push. And check the [Scrape.ly Store](https://scrape.ly/store.md) before you write anything: it has 568 ready-made Actors, and many sites don't need a browser at all. The [Web Scraper](https://scrape.ly/store/querystone/web-scraper.md) collected four book pages from books.toscrape.com in a 12-second run for $0.0029, with plain HTTP requests and one short extraction function.

If you'd rather have an AI agent drive a browser for you, see our guide to [Playwright MCP](https://scrape.ly/blog/playwright-mcp.md).

Scrape.ly Store Skip the browser code Scrape any public site with one page function: link following, proxies and retries included. $0.50 per 1,000 results.

[Try it free](https://scrape.ly/store/querystone/web-scraper.md) [Browse the Store](https://scrape.ly/store.md)

## FAQ

### Is Playwright better than Puppeteer for web scraping?

For new scrapers, yes. It auto-waits before every action, routes requests with a simple API, gives you a step-by-step Trace Viewer, and works in Python, Java and .NET as well as JavaScript. Puppeteer is a solid choice if you already have a Node.js scraper that only needs Chrome.

### Is Puppeteer faster than Playwright?

Not in a way you'd notice. In our test of the same three-page scraper, five runs each, the medians were 2.04 seconds for Playwright and 2.30 seconds for Puppeteer, a gap about the size of the network noise. Blocking images and stylesheets and running pages in parallel matter far more.

### Can I use Puppeteer's stealth plugin with Playwright?

Yes. `playwright-extra` accepts `puppeteer-extra-plugin-stealth` as a plugin, and our [Playwright stealth guide](https://scrape.ly/blog/playwright-stealth.md) has tested setup code. The plugin's last release was in March 2023, and it doesn't change your IP address or TLS fingerprint.

### Does Puppeteer work with Python?

Not officially. Puppeteer is a JavaScript library. If you scrape in Python, use Playwright for Python, which has both a sync and an async API.

### Should I migrate from Puppeteer to Playwright?

Only if it pays off. The APIs are close (`page.goto`, `page.click`, `page.evaluate` work much the same), so a port is usually quick, but a working scraper doesn't need rewriting. Good reasons to switch are flaky waits, the need for Python, or wanting the Trace Viewer for debugging.

### Which is harder for websites to detect?

Neither, out of the box. Both reported `navigator.webdriver` as `true` in headless mode in our test. Getting through bot protection depends on fingerprints, IP reputation and challenge solving, which is what the Scrape.ly browser handles for both libraries.

About the author

[Scrape.ly Team](https://scrape.ly/blog/author/scrapely-team.md) The team behind Scrape.ly

Scrape.ly is the web data platform from TeraShift GmbH in Switzerland, powered by Evomi: ready-made scrapers in the Scrape.ly Store, a cloud runtime, an API with JavaScript and Python SDKs, proxies, a hosted anti-bot browser and an MCP server for AI agents.

- [Website](https://scrape.ly/index.md)

Keep reading

## More in Guides

- [![Pixel-art robot with an orange >\_ on its terminal head walks out through an open iron gate whose arch carries the orange Cloudflare cloud logo, carrying a data card from the web page inside towards a crate](https://scrape.ly/media/bypass-cloudflare-scraping.png?w=750)](https://scrape.ly/blog/bypass-cloudflare-scraping.md)
  ### [How to Bypass Cloudflare When Scraping (Responsibly)](https://scrape.ly/blog/bypass-cloudflare-scraping.md)
  [What Cloudflare actually checks (bot scores, TLS fingerprints, challenge pages, Turnstile), from its own docs, why a plain HTTP request fails, and the responsible ways to scrape public pages behind it, up to a hosted browser that solves Turnstile.](https://scrape.ly/blog/bypass-cloudflare-scraping.md)
- [![Pixel-art robot with an orange >\_ on its terminal head, disguised in a dark fedora and a raised collar, pulls a data card out of a browser window showing the Playwright logo and drops it into a crate](https://scrape.ly/media/playwright-stealth.png?w=750)](https://scrape.ly/blog/playwright-stealth.md)
  ### [Playwright Stealth: How It Works, Tested Setup, and When It Is Not Enough](https://scrape.ly/blog/playwright-stealth.md)
  [Playwright stealth means patching a headless browser so it hides the signs of automation. The Node and Python packages, who maintains them, what they patch, tested setup code with real bot.sannysoft.com results, and where stealth stops working.](https://scrape.ly/blog/playwright-stealth.md)
- [![Pixel-art robot with an orange >\_ on its terminal head stands thinking between a house with lit windows on the left and a server rack of the same height on the right, a cable with data packets running to it from each](https://scrape.ly/media/residential-vs-datacenter-proxies.png?w=750)](https://scrape.ly/blog/residential-vs-datacenter-proxies.md)
  ### [Residential vs Datacenter Proxies for Web Scraping: Cost, Speed and When to Use Each](https://scrape.ly/blog/residential-vs-datacenter-proxies.md)
  [Residential vs datacenter proxies: residential IPs come from home internet connections and look like ordinary visitors; datacenter IPs come from servers and are faster and cheaper. How they compare, when to use which, real per-GB prices and two real runs.](https://scrape.ly/blog/residential-vs-datacenter-proxies.md)

## The internet is full of data. Start owning yours.

Scrape.ly gives you the infrastructure, the tooling, and the team to extract, process, and operationalize web data at any scale. Setup takes minutes. The advantage lasts forever.

[Get Started Free](https://my.scrape.ly/register) [Chat with Support](mailto:support@scrape.ly?subject=Question%20about%20Scrape.ly\&body=Hi%2C%20I%20have%20a%20question%20about%20Scrape.ly.)

No credit card. No setup calls.
