Playwright vs Puppeteer for web scraping
Playwright or Puppeteer for scraping? Choose Playwright for Firefox, WebKit and auto-waiting, Puppeteer for Chrome-only scripts. Compare code and port scripts.
- playwright
- puppeteer
- web scraping
- browser automation
- remote browser
On this page
- Playwright vs Puppeteer: which browsers and languages does each support?
- How do you run the same scraping tasks in Playwright and Puppeteer?
- Why the waiting code differs
- How do you connect Playwright and Puppeteer to a remote browser?
- How do browser contexts isolate cookies for multiple accounts or locales?
- How do request interception, proxies and test runners differ?
- How do you migrate a Puppeteer scraper to Playwright?
- Playwright vs Puppeteer: which one fits your scraping scenario?
- How to check maintenance health and speed
- Frequently asked questions
- Is Playwright better than Puppeteer for web scraping?
- Does Puppeteer support Firefox?
- Is Playwright faster than Puppeteer?
- Can I use Playwright and Puppeteer with a remote or hosted browser?
- Can I migrate a Puppeteer script to Playwright without a rewrite?
- What should you do next?
- Sources
Playwright vs Puppeteer comes down to your targets. Playwright is the better default for new scraping projects that need Firefox or WebKit, auto-waiting or parallel execution, according to Software Testing Tutorials Automation. Puppeteer is still a sound pick for Chrome-only scraping and automation, simple scripts and existing codebases, as Browserless’s comparison concludes.
You’re starting a scraper, or you have a Puppeteer script that works but now needs Firefox, WebKit or less hand-written waiting code.
Published comparisons disagree on whether Playwright is a Puppeteer fork and whether Puppeteer supports Firefox. The official documentation settles both. You’ll be able to pick a library per scenario, connect either one to a hosted browser over CDP, and port a script.
Key Takeaways
- Over a hosted CDP endpoint, test every advanced Playwright feature you rely on before you build on it.
- On a hosted CDP connection, the exit is fixed when you connect. A new context won’t change it, so each locale needs its own connection.
- Porting from Puppeteer is mostly mechanical. Waiting behavior and request interception are where scripts break.
- Speed and flakiness depend on your pages and pinned versions, so rerun the tasks yourself rather than trusting a published benchmark.
Playwright vs Puppeteer: which browsers and languages does each support?
Playwright drives Chromium, Firefox and WebKit from several languages, while Puppeteer’s README names Chrome and Firefox and describes a JavaScript library. Both projects ship often, so re-check the live docs before you rely on any cell below.

| Puppeteer | Playwright | |
|---|---|---|
| Browsers named | Chrome, Firefox | Chromium, Firefox, WebKit |
| Languages | JavaScript (Node.js) | Node.js, Python, Java, .NET |
| Protocol | DevTools Protocol or WebDriver BiDi | Its own protocol |
The README says Puppeteer controls “Chrome or Firefox over the DevTools Protocol or WebDriver BiDi.” That sentence names two browsers and no others.
Ranking pages disagree because they describe typical use. Browserless calls Puppeteer “Chrome-focused”, and another page says it suits “simple Chrome-based automation”. The README is the documented position.
Playwright’s own repositories describe a Python library, a Java library and the “official language port” for .NET, next to Node.js. Playwright’s Python repository opens with “Playwright is a Python library”.
As our Wayfern vs Multilogin comparison lays out, our hosted browser has no engine other than Chromium and no mobile emulation, so no Firefox and no WebKit. If you need those engines, run Playwright’s own browsers and keep our hosted anti-detect sessions for Chromium work.
Myth vs documented fact
- Playwright is a Puppeteer fork, or was built from scratch. Apify’s comparison says Playwright “began as a fork of Puppeteer but rapidly evolved into a library in and of itself.”
- Puppeteer is Chrome only, or has experimental Firefox and Edge. The README names Chrome and Firefox. It names no Edge.
- Puppeteer has a Python version. Puppeteer’s README calls it “a JavaScript library”. The pyppeteer repository calls itself an “Unofficial Python port”.
How do you run the same scraping tasks in Playwright and Puppeteer?
Playwright and Puppeteer run the same scraping tasks with near-identical code for navigation, interception, screenshots and PDFs. The real differences show up in form submission and in waiting for dynamic content.
The snippets target a placeholder URL. Each block shows Playwright first, then Puppeteer, so save them as separate files.
Record your own versions too. Run npm ls playwright puppeteer in your project and keep the output next to your results.
1. Navigation with a wait condition. Nothing changes here.
// Playwright
await page.goto("https://example.com/products", { waitUntil: "domcontentloaded" });
// Puppeteer
await page.goto("https://example.com/products", { waitUntil: "domcontentloaded" }); 2. Form fill and submit. Playwright’s fill() and click() wait for the element. Puppeteer needs you to pair the click with a navigation wait.
// Playwright
await page.fill("#q", "laptops");
await page.click("button[type=submit]");
// Puppeteer
await page.type("#q", "laptops");
await Promise.all([page.waitForNavigation(), page.click("button[type=submit]")]); 3. Dynamic content. Playwright asserts on a locator, and Puppeteer waits on a selector.
// Playwright (expect comes from @playwright/test)
const rows = page.locator(".result-row");
await expect(rows.first()).toBeVisible();
const titles = await rows.allTextContents();
// Puppeteer
await page.waitForSelector(".result-row", { visible: true });
const titles = await page.$$eval(".result-row", els => els.map(e => e.textContent)); 4. Blocking images. Register the handler before goto() in both.
// Playwright
await page.route("**/*", route =>
route.request().resourceType() === "image" ? route.abort() : route.continue());
// Puppeteer
await page.setRequestInterception(true);
page.on("request", req =>
req.resourceType() === "image" ? req.abort() : req.continue()); 5. Screenshot and PDF. The calls match.
// Playwright and Puppeteer
await page.screenshot({ path: "page.png", fullPage: true });
await page.pdf({ path: "page.pdf", format: "A4" }); In Playwright, page.pdf() has a browser limit: the Playwright API docs say PDF generation is “only supported in Chromium headless.” Firefox and WebKit runs need another route.
Why the waiting code differs
Playwright’s “Auto-waiting” page says Playwright “performs a range of actionability checks on the elements before making actions to ensure these actions behave as expected.” That is why snippet 2 has no explicit wait.
Puppeteer’s classic page methods leave the wait to you, so check its docs for your version.
Auto-waiting accounts for fewer timing failures, but it is not a measured result. A fair comparison of speed or flakiness needs you to pin:
- Library versions and the browser build each one launches.
- Hardware, CPU count and memory.
- Target pages, including whether they are static or script-heavy.
- Cold start versus warm start, and how many runs you averaged.
To rerun these tasks against a hosted browser, our guide to connecting Playwright or Puppeteer over CDP shows the connection step.
How do you connect Playwright and Puppeteer to a remote browser?
You connect Playwright or Puppeteer to a remote browser through one of three paths: Playwright’s connectOverCDP(), Playwright’s browserType.connect(), or Puppeteer’s puppeteer.connect() with a browserWSEndpoint.
Pick the path.
browserType.connect()targets a Playwright server, and Playwright’s BrowserType docs say the client’s major and minor version must match the server’s. A hosted CDP endpoint such as Wayfern’s speaks only CDP, soconnectOverCDP()andpuppeteer.connect()are your two options.Connect Playwright. Reuse the default context instead of creating a new one. Set
timeoutexplicitly, because Wayfern’s limits page gives a cold start of about 45 to 50 seconds, longer with a residential exit.
import { chromium } from "playwright";
const browser = await chromium.connectOverCDP(
`wss://api.wayfern.com/ws?token=${process.env.LINK_TOKEN}`,
{ timeout: 120_000 },
);
const page = await browser.contexts()[0].newPage(); - Connect Puppeteer. Pass the same URL as
browserWSEndpoint. Check the Puppeteer docs for which timeout option covers the initial connection.
import puppeteer from "puppeteer-core";
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://api.wayfern.com/ws?token=${process.env.LINK_TOKEN}`,
});
const page = await browser.newPage(); - Confirm success. The first message on the socket is a JSON “connected” message with a
connectionId, then plain CDP. There is no create-session REST call. Ifpage.goto()loads a page, you’re connected.
What to expect over a CDP connection:
- The browser presents a desktop device, so Playwright’s mobile device descriptors don’t apply.
- The BrowserType page calls
connectOverCDP()“significantly lower fidelity” than the native protocol. Test each native-protocol feature you rely on before you build on it.
To run both snippets against a real endpoint, mint a link token and set it as LINK_TOKEN.
How do browser contexts isolate cookies for multiple accounts or locales?
Browser contexts isolate cookies by giving each account or locale its own storage inside one shared browser instance. A browser context is what Playwright’s docs call an “isolated incognito-alike session” within a browser instance, with its own cookies, local storage and cache.
Playwright creates one with browser.newContext(), and Puppeteer uses browser.createBrowserContext(). Puppeteer’s BrowserContext reference says “Each context has isolated storage (cookies/localStorage/etc.)“.
Per-context options differ. Playwright’s newContext() takes locale, timezoneId, viewport and proxy.
In Puppeteer, createBrowserContext() takes a proxyServer option, while timezone and viewport are separate calls on the page: page.emulateTimezone() and page.setViewport().
A context doesn’t isolate the browser process or the network exit. Over remote CDP, the default context is browser.contexts()[0]. Whether you can create more depends on the endpoint, so test newContext() against yours before you build on it.
On Wayfern, the exit (country, region, city, isp and a sticky session) is set per connection on the /ws URL, as our guide to scraping behind logins and bot walls explains. Contexts don’t change it, so two locales mean two connections.
Cookies in one context stay out of the others, but both contexts ride the same connection and the same exit. For logins, the same guide covers holding one sticky residential exit.
Wayfern’s anti-detect sessions page points out that a cookie issued to one IP and replayed from another is an easy signal for a site to read. Keep each account’s context, cookies and exit together.
How do request interception, proxies and test runners differ?
Request interception, proxy scope and the test runner differ mainly in where each setting lives: Playwright can apply them to a whole browser context, while Puppeteer leans on the page and the launch flag.
| Criterion | Playwright | Puppeteer |
|---|---|---|
| Interception API | page.route(), context.route() | setRequestInterception plus request events |
| Interception scope | Page or whole context | Per page |
| Proxy setup | Launch option or per context | --proxy-server launch flag |
| Proxy credentials | In the proxy options | page.authenticate |
| Runner | Playwright Test, built in | External: Jest, Mocha or a script |
| Retries and traces | Built in | Bring your own |
Playwright’s BrowserContext docs say routing “provides the capability to modify network requests that are made by any page in the browser context.”
Puppeteer’s interception docs warn that once interception is on, “every request will stall unless it’s continued, responded or aborted.”
On a hosted browser, the proxy is set on the connection, not at launch. Wayfern’s CDP sessions take country, region, city and isp on the /ws URL, or your own http, https or socks5 proxy saved on a link. Credentials never go in the URL.
Playwright’s intro says Playwright Test “was created specifically to accommodate the needs of end-to-end testing.” Puppeteer’s README describes it as a JavaScript library, so you pick the runner.
Most scrapers need no runner: a plain script is enough. Retries and traces still help when your output is data. A trace recorded on a failed run shows which request or selector broke.
- Playwright: best for scrapers that need per-context routing, retries and traces.
- Puppeteer: best for small Chrome-only scripts that need only a launch flag and
page.authenticate.
How do you migrate a Puppeteer scraper to Playwright?
You migrate a Puppeteer scraper to Playwright by swapping the launch call, converting selectors to locators, and re-running the script against the same targets to diff the output.
- Swap the import and launch call. Replace
puppeteer.launch()withchromium.launch()fromplaywright. Stay on Chromium first so the library is the only variable. - Switch to locators. Swap
page.$andwaitForSelectorfor Playwright locators:page.locator(selector).click()waits for the element on its own. Delete your fixed sleeps. - Move interception to
route(). DropsetRequestInterception(true)and register a handler withpage.route(). - Move per-session options to
newContext(). Viewport, user agent, cookies and proxy settings belong on the context. - Re-run against the same targets. Save both scripts’ output as JSON and diff it. You’re done when the record counts and field values match.
| Puppeteer call | Playwright call | Note |
|---|---|---|
puppeteer.launch | chromium.launch | Import chromium from playwright |
page.$ | page.locator | Returns a locator, not a handle |
waitForSelector | locator actions | The action waits itself |
setRequestInterception | route() | No global switch to flip |
createBrowserContext | newContext | Options go here |
puppeteer.connect | connectOverCDP or connect | Pick by server type |
Four pitfalls break most ports:
- Strict locators. Playwright’s locator docs say locator operations “will throw an exception if more than one element matches.” Narrow the selector or use
first()on purpose. - Leftover sleeps. Old
setTimeoutwaits slow the port and hide real wait bugs. evaluatesignatures. Check how each library passes arguments into the page function.- Default timeouts. Set them explicitly instead of trusting either default.
Verify each call in the current Playwright API reference before you ship.
Playwright vs Puppeteer: which one fits your scraping scenario?
The right choice between Playwright and Puppeteer depends on your scraping scenario, and neither one wins every row below.
- QA cross-browser suite: Playwright. One test-oriented API covers Chromium, Firefox and WebKit. A tutorial from Software Testing Tutorials Automation says Playwright “supports multiple browsers (Chromium, Firefox, WebKit)“.
- Chrome-only scraping: Puppeteer. Browserless calls it “the simpler option for Chrome-only scraping and automation, with a slightly larger plugin ecosystem.” Stay on it unless a concrete limit appears.
- PDF and screenshot generation: either. Snippet 5 shows near-identical code in both libraries, so use whichever is already in your project.
- AI agent tooling: Playwright, with a check. Browserless describes Playwright’s MCP server as “an official Microsoft-maintained MCP server.” Read its README on GitHub for the tool list. We can’t point to a Puppeteer MCP server run by the Puppeteer project, so none is listed here.
- Existing Puppeteer codebase: stay. A working scraper costs nothing to keep. Migrate when you need Firefox or WebKit, or when hand-written waits keep breaking.
- Scraping through a hosted browser: match the host. On a Chromium host reached over CDP, Puppeteer is a natural fit, because
connectOverCDP()is Playwright’s lower-fidelity path.
Agents that only need web data can skip browser control. Wayfern’s MCP server is one such route: see live web data for AI agents.
How to check maintenance health and speed
We publish no benchmark numbers, because we have no run to cite. Star counts say little about upkeep, so compare release dates and open issues on the Playwright and Puppeteer GitHub releases pages yourself.
Then rerun your own targets. Record library versions, browser build, hardware, run count and network, and publish the script with the results.
If you’re comparing tools by billing unit, read the web scraping buyer’s guide.
Frequently asked questions
Is Playwright better than Puppeteer for web scraping?
For most new scraping projects, Playwright is the safer default: it supports Chromium, Firefox and WebKit and auto-waits before actions, per Software Testing Tutorials Automation. Puppeteer still fits Chrome-only scraping, PDF and screenshot jobs, and existing codebases. The scenario section maps each case to a pick.
Does Puppeteer support Firefox?
Yes. Puppeteer’s README names Firefox alongside Chrome, even though comparison sites describe Puppeteer as Chrome-focused. Confirm Firefox support for your pinned version before you depend on it.
Is Playwright faster than Puppeteer?
No cited benchmark settles it, so we won’t name a winner. Run both against your own targets with pinned versions, and compare wall-clock time and failure rates. The scraping tasks section has code you can adapt.
Can I use Playwright and Puppeteer with a remote or hosted browser?
Both attach to a remote browser over a WebSocket endpoint, and the connect call is the only code that changes. Test advanced features on the remote connection first. Our guide to anti-detect browser sessions over CDP shows a hosted setup.
Can I migrate a Puppeteer script to Playwright without a rewrite?
You rarely need a full rewrite, but you do need edits. Swap the launch call, convert selectors to locators, then diff the output against the same targets. Hand-written waiting code needs the most attention.
What should you do next?
Today: Pick your row in the scenario list. Then pin your library and browser versions in package.json, so later results are comparable.
This week: Port one script with the mapping table. Or leave the code alone and point it at a remote browser with connectOverCDP() in Playwright or puppeteer.connect() in Puppeteer.
This month: Rerun both libraries against your real targets and keep the results. Playwright vs Puppeteer is a close call on paper, and your own pages settle it.
For the remote-browser step, try Wayfern’s hosted anti-detect Chromium over CDP with either library. Plans and credit rates are on the Wayfern pricing page.
The honest catch: plans start at $19 a month, and Wayfern is Chromium only. Firefox and WebKit runs stay on your own machines.
Sources
Playwright vs Puppeteer: Which Is Better in 2026? · Playwright vs. Puppeteer: An Honest Comparison - Browserless · puppeteer/puppeteer: README · microsoft/playwright-dotnet: README · BrowserType | Playwright · Wayfern API reference · Wayfern API reference · Live Web Data for AI Agents | Wayfern. Every other source is linked where the article cites it.
See us more often in Google
One click marks Wayfern as a preferred source in Google, so our posts can show up more often in your Top Stories, AI Mode and AI Overviews.