Playwright vs Puppeteer vs Selenium, for scraping, comes down to this: pick Playwright for most new projects, because it waits for elements on its own, runs many isolated contexts in one browser and takes proxy credentials per context. Pick Puppeteer if you work in Node, target Chrome, and want the thinnest layer over the browser. Pick Selenium if you need Java, C# or Ruby bindings you already run, Safari, or an existing Selenium Grid. All three drive a real browser; the differences are in how much code you write around it.

This comparison covers languages and browsers, waiting, a small speed test we ran ourselves, the stealth ecosystem, and proxy support, which is where the three differ most.

Playwright vs Puppeteer vs Selenium at a glance

PlaywrightPuppeteerSelenium
Maintained byMicrosoftGoogle's Chrome teamThe Selenium project (open source)
Official languagesJavaScript/TypeScript, Python, Java, .NETJavaScript/TypeScriptJava, Python, C#, Ruby, JavaScript
BrowsersChromium, Firefox and WebKit builds it ships, plus branded Chrome and EdgeChrome and FirefoxChrome, Edge, Firefox and Safari, through each vendor's driver
Waiting for elementsAutomatic on locators and actionsAutomatic with page.locator(); older page.$ calls do not waitExplicit or implicit waits you configure
Isolated sessions per browserContexts, cheap to createBrowser contextsOne driver per browser session
Proxy per sessionPer context, credentials includedPer context, credentials via page.authenticate()Per driver; credentials need a workaround in Chrome
Scale-outYour own workersYour own workersSelenium Grid

Versions matter for several rows. This page reflects Playwright 1.63, Puppeteer 25.12 and Selenium 4.49, the versions current in late September 2026.

Languages and browsers

Playwright has first-party bindings for four languages, and they track the same release. A Python scraper and a Node one read almost the same, which helps when the team is mixed. It downloads its own browser builds; its Firefox and WebKit are patched to be driven, so they are close to, but not the same as, the browsers your visitors run. For branded Chrome, pass a channel or an executable path.

Puppeteer is JavaScript only. Python ports exist, but pyppeteer's last release on PyPI was in February 2024, so treat it as frozen. Puppeteer drives Chrome through the Chrome DevTools Protocol and Firefox through WebDriver BiDi.

Selenium covers the most languages and the most browsers, including Safari, because it speaks the W3C WebDriver standard that browser vendors implement. Selenium Manager downloads a matching driver for you, so the old chromedriver version dance is mostly gone.

If you need Safari or Ruby, the decision is made. Otherwise, move on to how much code each one makes you write.

Auto-wait and scraping ergonomics

Most flaky scrapers are timing bugs: the code looks for an element before the page has rendered it.

  • Playwright locators wait for an element to exist, be visible, stable and enabled before acting (its actionability checks), with one timeout for the whole step. page.locator(".price").inner_text() does the right thing on a slow page without extra code.
  • Puppeteer added a locator API that waits in a similar way. Code that uses page.$() or page.click(selector) does not wait and throws if the element is not there yet, so older examples need waitForSelector() first.
  • Selenium leaves waiting to you: an implicit wait on the driver, or WebDriverWait with an expected condition per step, as its waiting strategies page describes. It is explicit and predictable, and it is more code.

Tooling follows the same pattern. Playwright ships a recorder (codegen) and a trace viewer that replays a failed run step by step, which is the fastest way to see why a selector missed behind a proxy. Chrome's DevTools Recorder exports Puppeteer scripts. Selenium has Selenium IDE for recording and a large body of answers online.

Speed: a small benchmark we ran

Speed claims for these tools are mostly folklore, so we measured the part the tool controls: its own overhead, with the network taken out.

Setup, 2026-09-30. One Linux machine (AMD Ryzen 7 3700X, 32 GB RAM) that other jobs were also using, so treat the spread as real noise. Google Chrome 154 in headless mode for all four runs, so the browser was the same and only the driver differed. Playwright 1.63 from Python and from Node, Puppeteer 25.12 on Node 22, Selenium 4.49 from Python with chromedriver 154. The target was a tiny static HTML page served from the same machine.

  • Cold start: launch the browser, open a page, load the target, read one element, close. Median of 10 runs after a warm-up.
  • Per navigation: in one open page, load the target and read one element, 200 times in a row. Median per navigation.

Each tool ran five rounds; the table shows the range of the medians across those rounds.

ToolCold start, median (ms)Per navigation, median (ms)
Puppeteer (Node)290–49214.0–23.0
Selenium (Python)399–1,08217.9–20.9
Playwright (Python)346–62127.0–32.7
Playwright (Node)353–39926.8–36.3

What it says: on a local page, Puppeteer had the least overhead per navigation and Playwright the most, by roughly 10 to 20 milliseconds. Cold starts were a fraction of a second for all of them, with Selenium the noisiest.

What it does not say: anything about a real site. A page fetched from a remote server through a proxy spends most of its time on DNS, the proxy handshake, TLS and the download, and a few milliseconds of driver overhead disappear into that. We did not measure proxied remote pages here. If throughput matters, the bigger levers are how many contexts you run in parallel, blocking images and trackers you do not need, and waiting for domcontentloaded instead of the full load event.

Stealth and the anti-bot ecosystem

Out of the box, none of the three hides that it is automated. In our run, all three left navigator.webdriver set to true and a user agent containing HeadlessChrome in headless Chrome 154. A site that checks either one will spot all three the same way.

The community projects that patch these signals are where the tools differ, and their maintenance varies:

ToolCommon stealth add-onsLatest release we saw on the package registry, 2026-09-30
Puppeteerpuppeteer-extra with its stealth pluginMarch 2023 for both packages on npm
Playwrightplaywright-extra (reuses the Puppeteer plugins); patchright, a patched drop-in build; Camoufox, a modified Firefoxplaywright-extra March 2023; patchright September 2026; Camoufox September 2026
Seleniumundetected-chromedriver; SeleniumBase UC mode; nodriver, from the same author as undetected-chromedriverundetected-chromedriver February 2024; SeleniumBase September 2026; nodriver May 2026

A release date is not proof a tool works against a given site, only a hint about whether anyone is keeping up with browser changes. Test on your own targets.

Keep the limits in mind. These patches change what JavaScript on the page can see. They do not change your IP's reputation, your request rate, or the TLS fingerprint of the connection, and anti-bot systems score all of those. How to avoid getting blocked while scraping covers the full list, and TLS fingerprinting with curl_cffi explains the network-level part that no browser plugin touches.

Proxy support: per-context proxies and authentication quirks

This is where the three differ most, and where scraping code spends a surprising amount of time. Each of our integration guides was tested against local authenticating proxies; the table summarises what they found.

PlaywrightPuppeteerSelenium (Chrome)
Proxy for the whole browserlaunch(proxy=...)--proxy-server flag--proxy-server flag
Proxy per contextnew_context(proxy=...)createBrowserContext({ proxyServer })No; one driver per proxy
HTTP proxy with username and passwordFields in the proxy objectpage.authenticate() on every pageNot in the flag; use an IP allowlist or a small extension
Credentials in the proxy URLKeep them in the fields insteadnet::ERR_NO_SUPPORTED_PROXIESPage fails to load
SOCKS5 with credentialsRejected with an errorFails: Chrome offers no SOCKS authFails: same browser limit

The practical reading:

  • Playwright is the least work. One browser, one context per IP, credentials in the proxy object. The Playwright proxy guide has Python and Node code for launch-level and per-context proxies.
  • Puppeteer gets you per-context proxies too, but credentials go through page.authenticate() on each page, popups included, and a wrong password returns a 407 without throwing. The Puppeteer proxy guide covers the details.
  • Selenium with Chrome cannot send proxy credentials at all without help. The clean fix is an IP allowlist on the proxy; the in-code fix is a small Manifest V3 extension installed through WebDriver BiDi, because branded Chrome 137 and later ignores --load-extension. Firefox under Selenium does accept a BiDi auth handler. The Selenium proxy guide has both.

With any of them, an IP allowlist removes the credential problem entirely, and it is the only way to use SOCKS5 from a browser. On ProxyHive, ISP and datacenter IPs can be switched to allowlist authentication per IP in the dashboard. Static IPs also suit browser work: a logged-in session that keeps one address looks more ordinary than one that changes mid-flow, which rotating vs static proxies explains in more depth.

When to pick each

Pick Playwright when:

  1. You are starting a new scraper in Python, Node, Java or .NET.
  2. You need many isolated sessions, each with its own proxy, in one process.
  3. Pages render late and you want waiting handled for you.
  4. You want to debug failures from a trace instead of a screenshot.

Pick Puppeteer when:

  1. Your stack is Node and your target is Chrome.
  2. You want direct access to the Chrome DevTools Protocol for network interception or performance data.
  3. You are extending an existing Puppeteer codebase; rewriting it rarely pays for itself.

Pick Selenium when:

  1. Your team writes Java, C# or Ruby and already has Selenium infrastructure.
  2. You need Safari, or the exact browsers your users run, through their vendors' own drivers.
  3. You scale across machines with Selenium Grid.
  4. Your proxies can use an IP allowlist, which removes Selenium's biggest scraping pain.

And one case for none of them: if the data is in the HTML the server sends, or in a JSON call the page makes, skip the browser. A plain HTTP client is faster, cheaper in proxy traffic and easier to run. Check the page source and the network tab first; web scraping with proxies in Python starts there. Before you point any of them at a site, check that the job fits our allowed-use policy.