---
title: "Puppeteer vs Selenium for Web Scraping | Scrape.ly"
description: "Puppeteer vs Selenium for web scraping: languages, browsers, waiting, request blocking and parallel sessions, with the same scraper in both, tested."
url: https://scrape.ly/blog/puppeteer-vs-selenium
markdown: https://scrape.ly/blog/puppeteer-vs-selenium.md
---

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

# Puppeteer vs Selenium for Web Scraping: The Same Scraper in Both

Puppeteer vs Selenium for web scraping: languages, browsers, protocols, waiting, request blocking and parallel sessions, from the official docs on 28 October 2026, plus the same scraper written in Puppeteer and Selenium and tested on a JavaScript-rendered page.

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

- Published

  Oct 27, 2026

- Reading time

  7 min read

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

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](https://scrape.ly/blog/playwright-vs-puppeteer.md) and [Playwright vs Selenium](https://scrape.ly/blog/playwright-vs-selenium.md); for Selenium on its own, our [Selenium web scraping](https://scrape.ly/blog/selenium-web-scraping.md) 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](https://pptr.dev/) 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](https://www.selenium.dev/downloads/) 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](https://www.selenium.dev/documentation/webdriver/browsers/) 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`):

```javascript
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`):

```python
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:

```plaintext
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 going
```

Same 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 `$$eval` runs the mapping inside the page and returns plain data in one round trip. Selenium fetches element handles and reads each `.text` with 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](https://scrape.ly/blog/scrape-dynamic-websites.md) 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:

```javascript
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:

```python
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:

```plaintext
23 requests blocked; 20 books
20 books
0 images or stylesheets loaded
```

Both 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](https://scrape.ly/blog/puppeteer-stealth.md) 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](https://scrape.ly/blog/web-scraping-without-getting-blocked.md).

## 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](https://scrape.ly/blog/playwright-vs-puppeteer.md) and [Playwright vs Selenium](https://scrape.ly/blog/playwright-vs-selenium.md).

## 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](https://scrape.ly/store.md).
- **Deploy your own Puppeteer scraper.** Start from the [Crawlee + Puppeteer template](https://scrape.ly/templates/crawlee-puppeteer-chrome.md), and turn on the hosted [Scrapely browser](https://scrape.ly/docs/browser.md): 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](https://scrape.ly/templates.md) to start.

Scrape.ly Store Web Scraper When the data is already in the HTML, skip the browser: crawl a site over HTTP and run your own page function on each page, with link following, limits and structured exports.

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

## 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.

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 reaches up to take a JSON card with curly braces from a hook at the end of a cable that runs from a server rack, an amber bell ringing on top of the rack, a crate beside it](https://scrape.ly/media/webhooks-for-scraping.png?w=750)](https://scrape.ly/blog/webhooks-for-scraping.md)
  ### [Get Notified When a Scrape Finishes: Webhooks, Slack and Email](https://scrape.ly/blog/webhooks-for-scraping.md)
  [How to use webhooks for web scraping: get an HTTP call, a Slack or Discord message or an email the moment a scraper run finishes, with the run and dataset IDs to fetch results. Event types, payload templates, retries, a tested Python receiver and the API calls to set it up.](https://scrape.ly/blog/webhooks-for-scraping.md)
- [![Pixel-art robot with an orange >\_ on its terminal head pulls a data card out of a board headed by the Zapier logo, whose three linked steps show an orange lightning bolt, a web page and a green sheet, and drops it into a crate](https://scrape.ly/media/zapier-web-scraping.png?w=750)](https://scrape.ly/blog/zapier-web-scraping.md)
  ### [Web Scraping With Zapier: Send Scraped Data Anywhere](https://scrape.ly/blog/zapier-web-scraping.md)
  [How to connect web scraping to Zapier: start a Scrape.ly scraper from a Zap, catch the results when a run finishes, and send rows to Google Sheets, Slack, a CRM or any of Zapier's apps. Step-by-step recipes, the request each step sends, and when you don't need Zapier at all.](https://scrape.ly/blog/zapier-web-scraping.md)
- [![Pixel-art robot with an orange >\_ on its terminal head walks away from a robots.txt file with a robot face on its plate and rule lines ticked green or crossed red, through a raised red-and-white barrier towards a web page](https://scrape.ly/media/robots-txt-web-scraping.png?w=750)](https://scrape.ly/blog/robots-txt-web-scraping.md)
  ### [Robots.txt and Web Scraping: What It Means for Your Scraper](https://scrape.ly/blog/robots-txt-web-scraping.md)
  [What robots.txt means for web scraping: how the rules work under RFC 9309, how to read a real file, how to check URLs in Python (and where the standard library gets it wrong), what robots.txt does and doesn't decide legally, and how to respect it in a Scrape.ly crawl.](https://scrape.ly/blog/robots-txt-web-scraping.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.

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

No credit card. No setup calls.
