Puppeteer vs Selenium for web scraping: Puppeteer is the better pick when your scraper is JavaScript and the target works in Chrome or Firefox, because it talks to the browser directly, blocks requests in a few lines and runs isolated sessions inside one browser. Selenium is the better pick when you need another language (Python, Java, C# or Ruby), Safari or Edge, or you already run a Selenium Grid.
Below is how they compare on what matters for scraping, using pptr.dev and selenium.dev as of 28 October 2026, then the same scraper written in both and run against the same JavaScript-rendered page, plus a request-blocking test. If you're also weighing Playwright, see Playwright vs Puppeteer and Playwright vs Selenium; for Selenium on its own, our Selenium web scraping tutorial.
Puppeteer vs Selenium at a glance
| Puppeteer | Selenium | |
|---|---|---|
| Current version | 25.16.0 | 4.51.0 (9 October 2026) |
| Languages | JavaScript and TypeScript (Node.js) | Java, Python, C#, Ruby and JavaScript |
| Browsers | Chrome and Firefox | Chrome, Edge, Firefox, Safari and Internet Explorer |
| How it talks to the browser | Chrome DevTools Protocol or WebDriver BiDi | W3C WebDriver, through a driver per browser |
| Getting a browser | npm i puppeteer downloads a compatible Chrome | Selenium Manager downloads the matching driver (and browser) |
| Waiting | waitForSelector, waitForNavigation | WebDriverWait with expected conditions |
| Blocking requests | Built-in request interception | Through Chrome DevTools commands or BiDi |
| Many sessions | Browser contexts in one browser | Selenium Grid across machines |
| License | Apache 2.0 | Apache 2.0 |
Versions are from npm and PyPI on 28 October 2026.
Languages, browsers and protocols
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", and it "runs in the headless (no visible UI) by default". npm i puppeteer also downloads a compatible Chrome, so a fresh install works with no other setup. It's Node.js only: if your pipeline is Python, Puppeteer means a second runtime.
Selenium is a browser automation project built on the W3C WebDriver standard. Its core language bindings are .NET/C#, Ruby, Java, Python and JavaScript, all at 4.51.0, released 9 October 2026, and its documentation covers Chrome, Edge, Firefox, Internet Explorer and Safari. Selenium talks to each browser through that browser's driver. The old pain of matching driver versions by hand is gone: per the docs, "Selenium Manager is a command-line tool implemented in Rust that provides automated driver and browser management for Selenium. Selenium bindings use this tool by default".
For scraping, the protocol difference shows up in what you can reach. Puppeteer sits on the DevTools Protocol, so network events, request interception and browser contexts are first-class. Selenium's WebDriver model is about acting like a user; lower-level browser features come through browser-specific extras such as Chrome DevTools commands, or the newer WebDriver BiDi support.
The same scraper in both
The job: open the JavaScript version of quotes.toscrape.com, where the quotes only appear after a script runs, collect text, author and tags from the first two pages, and follow the "Next" link between them. Both scripts ran on 28 October 2026 on macOS, headless, with Chrome 156.
Puppeteer (Node.js 23, npm i puppeteer):
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
const quotes = [];
const start = Date.now();
await page.goto('https://quotes.toscrape.com/js/');
for (let n = 1; n <= 2; n++) {
await page.waitForSelector('.quote');
quotes.push(...await page.$$eval('.quote', (els) => els.map((el) => ({
text: el.querySelector('.text').textContent,
author: el.querySelector('.author').textContent,
tags: [...el.querySelectorAll('.tag')].map((t) => t.textContent),
}))));
const next = await page.$('li.next > a');
if (!next) break;
await Promise.all([page.waitForNavigation(), next.click()]);
}
console.log(quotes.length, 'quotes in', ((Date.now() - start) / 1000).toFixed(1), 's');
console.log(quotes[10].author, '|', quotes[10].text.slice(0, 60));
await browser.close();Selenium (Python 3.12, pip install selenium):
import time
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
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 10)
quotes = []
start = time.time()
driver.get("https://quotes.toscrape.com/js/")
for n in range(1, 3):
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".quote")))
for el in driver.find_elements(By.CSS_SELECTOR, ".quote"):
quotes.append({
"text": el.find_element(By.CSS_SELECTOR, ".text").text,
"author": el.find_element(By.CSS_SELECTOR, ".author").text,
"tags": [t.text for t in el.find_elements(By.CSS_SELECTOR, ".tag")],
})
links = driver.find_elements(By.CSS_SELECTOR, "li.next > a")
if not links:
break
old = driver.find_element(By.CSS_SELECTOR, ".quote")
links[0].click()
wait.until(EC.staleness_of(old))
print(len(quotes), "quotes in", f"{time.time() - start:.1f}", "s")
print(quotes[10]["author"], "|", quotes[10]["text"][:60])
driver.quit()Output, Puppeteer 25.16.0 first, then Selenium 4.51.0:
20 quotes in 1.7 s
Marilyn Monroe | “This life is what you make it. No matter what, you're going
20 quotes in 2.0 s
Marilyn Monroe | “This life is what you make it. No matter what, you're goingSame 20 quotes, same order, in about the same time. Puppeteer ran its own downloaded Chrome 156.0.8078.47; Selenium drove the installed Chrome 156.0.8078.48 through chromedriver 156.0.8078.73, which Selenium Manager supplied. On two pages, browser start-up dominates, so don't read the 0.3-second gap as a speed ranking.
What the code shows:
- Extraction runs in different places. Puppeteer's
$$evalruns the mapping inside the page and returns plain data in one round trip. Selenium fetches element handles and reads each.textwith its own command, which adds up on pages with thousands of elements. - Waiting after a click. Puppeteer pairs the click with
waitForNavigation(). Selenium has no single equivalent here, so the script waits until the old quote goes stale, a common Selenium pattern. - Language fit. Puppeteer needs Node.js. If the rest of your pipeline is Python (pandas, a database loader, an LLM step), Selenium keeps it in one language.
Waiting for content
JavaScript pages load in stages, so a scraper has to wait for the element it needs, not a fixed number of seconds. Puppeteer's page.waitForSelector() resolves when the element appears, and navigation helpers wait for page loads. Selenium's WebDriverWait polls an expected condition such as presence_of_element_located, visibility_of_element_located or staleness_of until it's true or the timeout passes. Both work well once you use them consistently; the classic bug in both is a sleep() that's too short on a slow day. Our guide to scraping dynamic websites covers when you need a browser at all.
Blocking images and stylesheets
Skipping images, fonts and CSS cuts bandwidth and speeds up scraping. Both tools can do it on Chrome; the code looks different.
Puppeteer has request interception built in:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
let blocked = 0;
await page.setRequestInterception(true);
page.on('request', (req) => {
if (['image', 'font', 'stylesheet'].includes(req.resourceType())) { blocked++; req.abort(); }
else req.continue();
});
await page.goto('https://books.toscrape.com/');
console.log(blocked, 'requests blocked;', await page.$$eval('article.product_pod', (els) => els.length), 'books');
await browser.close();Selenium reaches the same Chrome feature through a DevTools command, which works with Chrome and Edge:
from selenium import webdriver
from selenium.webdriver.common.by import By
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)
driver.execute_cdp_cmd("Network.enable", {})
driver.execute_cdp_cmd("Network.setBlockedURLs", {"urls": ["*.jpg", "*.png", "*.css"]})
driver.get("https://books.toscrape.com/")
print(len(driver.find_elements(By.CSS_SELECTOR, "article.product_pod")), "books")
print(driver.execute_script("return performance.getEntriesByType('resource').filter(r => r.transferSize > 0 && /\\.(jpg|png|css)/.test(r.name)).length"), "images or stylesheets loaded")
driver.quit()Our runs on 28 October 2026:
23 requests blocked; 20 books
20 books
0 images or stylesheets loadedBoth kept all 20 books and dropped the heavy resources. The difference: Puppeteer decides per request by resource type and can also modify or mock responses; Selenium's Chrome command blocks by URL pattern and ties your script to Chromium browsers.
Running many sessions
A scraper that handles many accounts, locales or sites at once needs isolated sessions. Puppeteer creates them inside one browser with browser.createBrowserContext(), each with its own cookies and storage, which is light on memory. Selenium scales the other way: each WebDriver session is its own browser, and Selenium Grid spreads sessions across machines and browser types. Grid is the stronger answer for cross-browser testing at scale; for scraping one site from many sessions, contexts are cheaper.
Detection and blocking
Neither tool is invisible. Headless Chrome under automation exposes signals that anti-bot systems check, whichever library drives it. Community plugins such as puppeteer-extra-plugin-stealth patch some of them; our Puppeteer stealth guide explains what they change and where they stop working. For Selenium there are community patched drivers too. In practice, the IP address and request rate decide more blocks than the library: residential proxies and a sensible pace matter more than the choice between the two. See web scraping without getting blocked.
Which should you pick?
- Pick Puppeteer if your scraper is JavaScript or TypeScript, Chrome or Firefox is enough, and you want request interception and lightweight parallel sessions out of the box.
- Pick Selenium if your team writes Python, Java, C# or Ruby, you need Safari or Edge, or you already run a Selenium Grid.
- Consider Playwright if you want both: it covers Node.js, Python, Java and .NET with auto-waiting and request interception. We compare it in Playwright vs Puppeteer and Playwright vs Selenium.
Skip the browser plumbing with Scrape.ly
Whichever library you choose, the hard part of scraping at scale is the browser fleet: proxies, fingerprints, CAPTCHAs, memory, retries and schedules. On Scrape.ly you can skip it in two ways:
- Use a ready-made scraper. For popular sites, a Store Actor already returns structured data, with no browser code to maintain. Browse the Store.
- Deploy your own Puppeteer scraper. Start from the Crawlee + Puppeteer template, and turn on the hosted Scrapely browser: your Puppeteer and Playwright code connects to it without changes, with anti-bot fingerprints and proxies handled. Selenium scripts aren't covered by that automatic connection, which is one more reason to write new scrapers in Puppeteer or Playwright.
Schedules, webhooks, an API and an MCP server for AI agents come with every run. Browse all web scraping code templates to start.
FAQ
Is Puppeteer better than Selenium for web scraping?
For JavaScript scrapers on Chrome or Firefox, usually yes: it talks to the browser over the DevTools Protocol or WebDriver BiDi, has request interception built in and runs isolated contexts in one browser. Selenium is the better choice for Python, Java, C# or Ruby, and for Safari or Edge.
Is Puppeteer faster than Selenium?
In our two-page test, Puppeteer took 1.7 seconds and Selenium 2.0, with browser start-up dominating both. Puppeteer's in-page extraction makes fewer round trips, which helps on large pages, but proxies and target-site speed matter more in real scraping.
Can I use Puppeteer with Python?
Puppeteer itself is a Node.js library. In Python, use Selenium or Playwright for Python, which has a similar API to Puppeteer.
Does Selenium still need chromedriver?
Selenium still uses a driver per browser, but Selenium Manager, shipped with every Selenium release, finds or downloads the matching driver automatically. In our run it supplied chromedriver 156 for Chrome 156 with no setup.
Can Puppeteer and Selenium scrape JavaScript-rendered pages?
Yes. Both drive a real browser, so the page's scripts run before you read it. Wait for the element you need with waitForSelector or WebDriverWait, as in the scripts above.
Which is easier to learn for scraping?
Puppeteer has a smaller API and one language, so a first scraper is quick to write if you know JavaScript. Selenium's API is larger but the same across five languages, and it has the longest history of tutorials.




