Playwright vs Selenium for web scraping: Playwright is the better default for a new scraper, because it waits for elements on its own, intercepts network requests in one line and runs many isolated sessions inside one browser. Selenium remains a solid choice when you need a language Playwright doesn't offer, such as Ruby, or you already run a Selenium Grid.
Below is how they compare on what matters for scraping, using facts from playwright.dev and selenium.dev as of 22 October 2026, then the same scraper written in both and run on the same JavaScript-rendered page. At the end: how to skip the browser plumbing entirely.
Playwright vs Selenium at a glance
| Playwright | Selenium | |
|---|---|---|
| Languages | TypeScript/JavaScript, Python, .NET, Java | Java, Python, C#, Ruby, JavaScript, Kotlin |
| Browsers | Chromium, Firefox and WebKit builds it installs, plus installed Chrome and Edge | Chrome, Edge, Firefox, Safari, Internet Explorer |
| Setup | pip install playwright, then playwright install downloads matching browsers | pip install selenium; Selenium Manager finds or fetches the driver |
| Waiting | Auto-waits before every action (visible, stable, enabled, receives events) | Implicit wait (default 0) or explicit waits you write |
| Network interception | Built in: page.route() to block, mock or modify requests | WebDriver BiDi network handlers, enabled in options, still being rolled out |
| Many sessions | Browser contexts: isolated profiles inside one browser | One driver per session; Selenium Grid spreads sessions across machines |
| Sync and async (Python) | Both | Synchronous |
navigator.webdriver in headless mode | true (our test) | true (our test) |
Languages and browsers
Playwright is "available for TypeScript, Python, .NET, and Java" and drives Chromium, Firefox and WebKit with one API (playwright.dev). Each Playwright release is tied to specific browser builds that playwright install downloads, and it can also drive the Chrome or Edge installed on your machine. It doesn't drive branded Firefox or Safari, because it relies on patched builds of those engines (browsers docs).
Selenium implements the W3C WebDriver standard, so each browser vendor's driver speaks the same protocol. Its docs show bindings for Java, Python, C#, Ruby, JavaScript and Kotlin, and it supports Chrome, Edge, Firefox, Safari and Internet Explorer (selenium.dev). Selenium Manager, a tool written in Rust, now finds or downloads the right driver automatically, which removed the old "download chromedriver by hand" step.
For scraping, the browser list rarely matters: almost every scraper runs Chromium. The language list matters more, for example if your team works in Ruby.
The same scraper in both
The task: scrape the first three pages of quotes.toscrape.com/js, a sandbox site whose quotes are written into the page by JavaScript, so a plain HTTP request sees no quotes at all. Install with pip install playwright selenium, then run playwright install chromium.
Playwright (sync API):
from urllib.parse import urljoin
from playwright.sync_api import sync_playwright
START_URL = "https://quotes.toscrape.com/js/"
def scrape(max_pages: int = 3) -> list[dict]:
quotes = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Skip images, fonts and stylesheets: the data is in the HTML.
page.route(
"**/*",
lambda route: route.abort()
if route.request.resource_type in {"image", "font", "stylesheet"}
else route.continue_(),
)
url = START_URL
for _ in range(max_pages):
page.goto(url)
cards = page.locator("div.quote")
cards.first.wait_for() # the quotes are written by JavaScript
for card in cards.all():
quotes.append({
"text": card.locator("span.text").inner_text(),
"author": card.locator("small.author").inner_text(),
"tags": card.locator("a.tag").all_inner_texts(),
})
next_link = page.locator("li.next > a")
if next_link.count() == 0:
break
url = urljoin(page.url, next_link.get_attribute("href"))
browser.close()
return quotes
if __name__ == "__main__":
rows = scrape()
print(len(rows), rows[0])Selenium:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
START_URL = "https://quotes.toscrape.com/js/"
def scrape(max_pages: int = 3) -> list[dict]:
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options) # Selenium Manager finds the driver
wait = WebDriverWait(driver, timeout=10)
quotes = []
try:
url = START_URL
for _ in range(max_pages):
driver.get(url)
cards = wait.until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div.quote"))
)
for card in cards:
quotes.append({
"text": card.find_element(By.CSS_SELECTOR, "span.text").text,
"author": card.find_element(By.CSS_SELECTOR, "small.author").text,
"tags": [t.text for t in card.find_elements(By.CSS_SELECTOR, "a.tag")],
})
next_links = driver.find_elements(By.CSS_SELECTOR, "li.next > a")
if not next_links:
break
url = next_links[0].get_attribute("href") # Selenium returns the absolute URL
finally:
driver.quit()
return quotes
if __name__ == "__main__":
rows = scrape()
print(len(rows), rows[0])We ran both on 22 October 2026 on macOS with Python 3.14, Playwright 1.64.0 and Selenium 4.51.0 (driving the installed Google Chrome). Both printed the same thing:
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 code is similar in length. The differences are in what you didn't have to write, and what each line protects you from.
Waiting for content
This is where most scrapers break. Selenium's own docs call race conditions between the browser and your script "one of the primary causes of flaky tests" (waits docs). Its implicit wait defaults to 0, so a lookup fails immediately if the element isn't there yet. You add explicit waits like the WebDriverWait above at every point where content loads, and the docs warn: "Do not mix implicit and explicit waits."
Playwright runs actionability checks before every action. A click waits until the element is visible, stable, enabled and not covered by another element, and fails with a TimeoutError only if that doesn't happen in time (auto-waiting docs). In a scraper you still wait once for the data to appear (cards.first.wait_for() above), but clicks, form fills and pagination stop being a source of random failures.
Network interception
Playwright can watch, block, mock or modify every request a page makes with page.route() (network docs). In the scraper above, one call stops images, fonts and stylesheets from loading, which saves bandwidth on every page, and that matters when you pay for proxy traffic. The same API lets you capture the JSON responses a page fetches, which is often cleaner than parsing the HTML.
Selenium is adding the same abilities through WebDriver BiDi: request and response handlers you enable in the browser options. Its docs note that the implementation is still being tracked (BiDi network docs). For scraping today, Playwright's version is the one you can rely on in every language.
Running many sessions
Scrapers need parallel, isolated sessions: separate cookies, separate logins, separate proxies. Playwright's browser contexts are exactly that, "equivalent to a brand new browser profile", created inside one running browser "with near-zero overhead" (playwright.dev), and each context can have its own proxy. Its async Python API lets one process drive many pages at once.
Selenium starts one browser per driver session. To run many, you start many browsers, or run Selenium Grid, which "is for you" if you want to run "in parallel across multiple machines" (Selenium docs). Grid is powerful, but it is infrastructure you operate.
Detection and blocking
Neither tool hides that it is automated, and neither project markets itself for that. Playwright "was created specifically to accommodate the needs of end-to-end testing" (intro). The W3C WebDriver standard defines a navigator.webdriver property that reports true when a browser is under automation (spec). In our test on the same machine, it was true in headless Playwright Chromium, headless Playwright Firefox and headless Selenium Chrome.
So on sites with serious bot protection, the choice between Playwright and Selenium doesn't decide whether you get through. Fingerprints, IP reputation and challenge pages do. That's a separate layer, and it's where most scraping projects spend their time.
Selenium vs Playwright: which should you pick?
- Starting a new Python or JavaScript scraper: Playwright. Less waiting code, built-in request blocking, cheap isolated sessions.
- Your team writes Ruby: Selenium, since Playwright has no official Ruby version.
- You already run Selenium Grid for testing: reuse it for scraping, or move the scrapers to Playwright when the waits start costing you.
- You need real Safari: Selenium. Playwright drives its own WebKit build instead.
- The site blocks headless browsers: neither library alone. You need a browser with real fingerprints and good IPs, covered below.
Puppeteer vs Selenium, and Playwright vs Puppeteer
Puppeteer is a "JavaScript library which provides a high-level API to control Chrome or Firefox over the DevTools Protocol or WebDriver BiDi". Against Selenium it trades breadth for focus: one language and two browsers, versus Selenium's six languages and five browsers. Against Playwright, the main difference for scrapers is language: Puppeteer is JavaScript-only, while Playwright also has Python, .NET and Java. We cover Playwright vs Puppeteer in depth in a separate article.
Skip the browser plumbing with Scrape.ly
Picking the library is the easy part. Keeping a browser scraper running means proxies, fingerprints, retries, scheduling and storage. Scrape.ly gives you three ways to skip that work:
- Start from a template. The Crawlee + Playwright (Python) template gives you a
PlaywrightCrawlerwith a request queue, proxy configuration and an output schema:scrapely create my-actor --template crawlee-playwright-py, put your parsing in the router, and push it. It runs in the cloud with the same storage, scheduling and API as every Store Actor. More starters are on the templates page. - Run the browser on the Scrape.ly browser. Turn on the Scrape.ly browser for your Actor and rebuild: every Playwright or Puppeteer launch connects to a hosted Chromium with anti-bot fingerprints, Cloudflare Turnstile solving and its own residential proxy, with no code changes. You choose the exit country, state or city and the operating system. 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. See anti-blocking for how it fits with the managed proxy.
- Use a ready Actor. For big sites, someone has already done it. The Scrape.ly Store has 568 Actors, and many don't need a browser at all: in our Google Maps tutorial, the Google Maps Scraper saved 10 places with 11 plain HTTP requests and under 1 MB of traffic.
If you'd rather have an AI agent drive the browser, see our guide to Playwright MCP.
FAQ
Is Playwright better than Selenium for web scraping?
For new scrapers, yes. Auto-waiting removes the most common source of flaky runs, page.route() lets you block heavy resources or capture JSON responses, and browser contexts give you cheap isolated sessions. Selenium is the better fit only if you need Ruby or real Safari, or already operate a Selenium Grid.
Is Playwright faster than Selenium?
We didn't benchmark them, and single runs on one machine wouldn't prove much. Architecturally, Playwright avoids starting a new browser for every session by using contexts, and lets you block images and stylesheets, which saves time and bandwidth on every page.
Can Playwright replace Selenium?
For scraping, almost always. Playwright covers the same tasks (navigation, clicks, forms, screenshots, JavaScript evaluation) and adds network interception and contexts. The exceptions are teams that need Selenium's language list, real Safari, or an existing Grid.
Does Playwright work with Python?
Yes. Playwright for Python has both a synchronous and an asynchronous API and needs Python 3.8 or newer. Install it with pip install playwright, then playwright install to download the browsers.
Which is harder for websites to detect, Playwright or Selenium?
Neither by default. Both report navigator.webdriver as true in headless mode in our test. Getting past bot protection depends on fingerprints, IPs and challenge solving, which is what the Scrape.ly browser handles.




