     [Blog](https://scrapfly.io/blog)   /  [headless-browser](https://scrapfly.io/blog/tag/headless-browser)   /  [Puppeteer vs Selenium for Web Scraping: Tested on the Same Chrome](https://scrapfly.io/blog/posts/puppeteer-vs-selenium)   # Puppeteer vs Selenium for Web Scraping: Tested on the Same Chrome

 by [Mayada Shaaban](https://scrapfly.io/blog/author/mayada-shaaban-90143e67) Nov 12, 2026 26 min read [\#headless-browser](https://scrapfly.io/blog/tag/headless-browser) [\#puppeteer](https://scrapfly.io/blog/tag/puppeteer) [\#selenium](https://scrapfly.io/blog/tag/selenium) 

 [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fpuppeteer-vs-selenium "Share on LinkedIn") [  ](https://x.com/intent/tweet?url=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fpuppeteer-vs-selenium&text=Puppeteer%20vs%20Selenium%20for%20Web%20Scraping%3A%20Tested%20on%20the%20Same%20Chrome "Share on X") [  ](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fpuppeteer-vs-selenium "Share on Facebook")    

 

 

Summarize this article with

 [  ](https://chat.openai.com/?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fpuppeteer-vs-selenium) [  ](https://claude.ai/new?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fpuppeteer-vs-selenium) [  ](https://x.com/i/grok?text=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fpuppeteer-vs-selenium) [  ](https://www.perplexity.ai/search/new?q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fpuppeteer-vs-selenium) [  ](https://www.google.com/search?udm=50&aep=11&q=Summarize%20this%20article%20and%20explain%20how%20Scrapfly%20helps%20me%20scrape%20any%20website%20at%20scale%20and%20bypass%20anti-bot%20systems%20for%20my%20use%20case%3A%20https%3A%2F%2Fscrapfly.io%2Fblog%2Fposts%2Fpuppeteer-vs-selenium) 



         

When it comes to web scraping with a real browser, there are generally two popular tools: Puppeteer, which controls Chrome through the Chrome DevTools Protocol, and Selenium, which controls almost any browser through the WebDriver protocol.

So which one is faster? Not by much, as in our test on the same Chrome 156 both tools scraped five product pages just 79 ms apart. However, when it comes to reading single elements Selenium is much slower - about five times slower than Puppeteer, which can add up quickly in bigger scrapers!

We'll compare both tools by writing the same scraper in Puppeteer and Selenium. We'll cover common questions like how each tool works, which one is faster and whether Puppeteer can be used with Python, as well as some advanced tasks like request interception and blocking and how each one looks to fingerprinting tests.



## Key Takeaways

- Puppeteer and Selenium scraped five pages within 4% of each other on the same Chrome, because page loads dominate a scrape.
- A single element read took 0.93 ms in Puppeteer and 4.93 ms in Selenium, so Selenium scrapers should read rows in bulk with `execute_script`.
- Puppeteer is a Node.js library, and its Python port pyppeteer hasn't shipped a release since February 2024, so Python teams should use Selenium or Playwright.
- Puppeteer intercepts requests in its main API, while Selenium blocks requests on Chrome through a DevTools command.
- Puppeteer connects to [Scrapfly Cloud Browser](https://scrapfly.io/products/cloud-browser-api) with `puppeteer.connect` and a WebSocket URL, while Selenium can't because Cloud Browser has no WebDriver endpoint.

**Get web scraping tips in your inbox**Trusted by 100K+ developers and 30K+ enterprises. Unsubscribe anytime.







## Puppeteer vs Selenium at a Glance

Puppeteer is a Node.js library made by the Chrome team to control Chrome and Firefox directly, while Selenium was initially a tool created to test websites and quickly became a general browser automation framework for almost every language and browser.

So which one to choose? We recommend picking by the programming language and the browsers your project needs first, as when it comes to scraping speed both tools were almost identical in our test.

Let's take a look at how both tools compare at a glance:

|  | Puppeteer | Selenium |
|---|---|---|
| Maintainer | Chrome Browser Automation team (Google) | Selenium project (Software Freedom Conservancy) |
| Languages | JavaScript, TypeScript (Node.js) | Java, Python, C#, Ruby, JavaScript |
| Browsers | Chrome, Firefox | Chrome, Edge, Firefox, Safari |
| Protocol | CDP, WebDriver BiDi | WebDriver, WebDriver BiDi, CDP (Chromium only) |
| Browser setup | Downloads Chrome for Testing | Selenium Manager fetches driver and browser |
| Launch (our test) | 462 ms | 711 ms |
| 5-page scrape (our test) | 2.21 s | 2.29 s |
| One element read (our test) | 0.93 ms | 4.93 ms |
| bot.sannysoft.com (our test) | 4 failed checks headless, 1 headful | 4 failed checks headless, 1 headful |

Note that the numbers in this table come from our own test (more on that in the speed section below), where both tools ran on the same machine and the same Google Chrome 156 browser (157 for the bot.sannysoft.com row).

[How to Web Scrape with Puppeteer and NodeJS in 2026Introduction to using Puppeteer in Nodejs for web scraping dynamic web pages and web apps. Tips and tricks, best practices and example project.](https://scrapfly.io/blog/posts/web-scraping-with-puppeteer-and-nodejs)

[Web Scraping with Selenium and PythonIntroduction to web scraping dynamic javascript powered websites and web apps using Selenium browser automation library and Python.](https://scrapfly.io/blog/posts/web-scraping-with-selenium-and-python)



## How Do Puppeteer and Selenium Control the Browser?

Puppeteer and Selenium use two different browser automation protocols. Puppeteer uses the [Chrome DevTools Protocol](https://scrapfly.io/blog/posts/chrome-cdp-stealth-browser-automation-detection-explained) (aka CDP) - a high-level protocol that allows programs to control Chrome through a single socket connection.

Selenium on the other hand uses WebDriver - a W3C protocol where a middleware program called a driver (ChromeDriver for Chrome, geckodriver for Firefox) sits between our code and the browser, translating each command to browser actions.



The CDP connection also works both ways, so the browser can send messages back to Puppeteer on its own. This means that things like network requests, responses and console messages arrive as events while the page is loading, so our scraper can react to them right away.

With Selenium, however, each command is sent to the driver as its own HTTP request - be it `find_element`, `.text` or `get`. Let's see this in action by turning on Selenium's debug log and reading a single product title:

python

```python
import logging

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.get("https://web-scraping.dev/products")

# print every HTTP request Selenium sends to ChromeDriver from here on
logging.basicConfig(format="%(message)s")
logging.getLogger("selenium.webdriver.remote.remote_connection").setLevel(logging.DEBUG)

title = driver.find_element(By.CSS_SELECTOR, ".product h3").text
print(title)
driver.quit()
```



In the example above, we're turning on the debug logs of Selenium's `remote_connection` module just before we read the title, which will print every request our code sends to ChromeDriver. If we run it we'll see two requests for that one line of code (we trimmed the "Finished Request" lines, response headers and the closing `DELETE` request and shortened the IDs):

text

```text
POST http://localhost:50086/session/554dfd28.../element {'using': 'css selector', 'value': '.product h3'}
Remote response: status=200 | data={"value":{"element-6066-11e4-a52e-4f735466cecf":"f.389A0E62...e.5"}}
GET http://localhost:50086/session/554dfd28.../element/f.389A0E62...e.5/text {}
Remote response: status=200 | data={"value":"Box of Chocolate Candy"}
Box of Chocolate Candy
```



As you can see, the first request finds the element and gets back its reference, and only the second one asks for the text - so that's two trips to ChromeDriver just to read one title! Not to mention, the driver is a separate program and it has to match our Chrome version too.

What about WebDriver BiDi? This is the newer version of the WebDriver protocol which can send messages both ways (just like CDP), and it's already used by both projects. Puppeteer uses it to control Firefox, while Selenium 4 uses it for events and network interception across browsers - pretty much what Puppeteer gets through CDP!

[Is Chrome CDP Detectable? CDP vs WebDriver for Browser AutomationChrome CDP is not stealth by itself. Learn how CDP, WebDriver, Playwright, Puppeteer, nodriver, Camoufox, and cloud browsers affect detection.](https://scrapfly.io/blog/posts/chrome-cdp-stealth-browser-automation-detection-explained)



## Is Puppeteer Faster Than Selenium?

Yes, though not by as much as you'd think. Puppeteer launched faster and read single elements about five times faster, however when scraping five pages both tools finished within 4% of each other as most of the time is spent waiting for pages to load.

So where does Selenium lose time? In loops that read many elements one by one, as every read is two WebDriver calls (find, then `.text`) while Puppeteer reads it right inside the page.

For this test, we'll be using a simple web-scraping task: both tools open the first five pages of `web-scraping.dev/products` and collect all 25 product titles. We'll keep it short and also read a single title 200 times to see how long each read takes (through `$eval` in Puppeteer and `find_element` plus `.text` in Selenium).

Now that we have our test task let's give it a go! We ran each tool five times, taking turns between Puppeteer and Selenium after one warm-up run each. For this we used Puppeteer 25.16.0 with Node.js 26.5 and Selenium 4.52.0 with Python 3.14 - both controlling the same headless Google Chrome 156 on an Apple M5 MacBook. Here are the median results:

| Step (median of 5) | Puppeteer | Selenium |
|---|---|---|
| Launch browser | 462 ms | 711 ms |
| Scrape 5 pages | 2,209 ms | 2,288 ms |
| Read one element (200 reads) | 0.93 ms | 4.93 ms |

We can see that Selenium is about 250 ms slower to start the browser - most of that is the time it takes to find and start ChromeDriver. That's not much for a long scrape, though if we start 1,000 short sessions that adds up to about four extra minutes.

Element reads are a whole other story. For example, to read 1,000 fields one by one Selenium would need almost 5 seconds while Puppeteer does it in under one!

A big part of this time is spent going back and forth with ChromeDriver. Let's get rid of it by reading all rows in one `execute_script` call instead and compare it to reading five product rows (title and price) field by field:

python

```python
import time

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.get("https://web-scraping.dev/products")

# one driver call per find and per .text: 1 + 5 * 4 = 21 calls
start = time.perf_counter()
rows = []
for product in driver.find_elements(By.CSS_SELECTOR, ".product"):
    title = product.find_element(By.CSS_SELECTOR, "h3").text
    price = product.find_element(By.CSS_SELECTOR, ".price").text
    rows.append((title, price))
print(f"per-element: {len(rows)} rows in {(time.perf_counter() - start) * 1000:.1f} ms")

# one driver call: the browser builds the whole list and sends it back
start = time.perf_counter()
rows = driver.execute_script("""
    return [...document.querySelectorAll('.product')].map(p => [
        p.querySelector('h3').innerText, p.querySelector('.price').innerText
    ]);
""")
print(f"execute_script: {len(rows)} rows in {(time.perf_counter() - start) * 1000:.1f} ms")
driver.quit()
```



In our code example above, we have two ways of reading the same five rows:

- the `find_elements` loop - makes 21 driver calls, one for every find and every `.text`
- `execute_script` - makes a single call and lets the browser build the whole list

If we run it we'll see:

text

```text
per-element: 5 rows in 69.5 ms
execute_script: 5 rows in 2.6 ms
```



Here we can see how using `execute_script` sped up our reads more than 25 times (two more runs gave 70.9 ms vs 2.7 ms and 70.1 ms vs 4.2 ms).

So, is Selenium slow? Not really when running locally, as a find plus `.text` cost us under 5 ms. However, remote setups like [Selenium Grid](https://scrapfly.io/blog/posts/intro-to-web-scraping-using-selenium-grid) add a network round trip to every single command, so bulk reads matter even more there.

[Intro to Web Scraping Using Selenium GridIn this guide, you will learn about installing and configuring Selenium Grid with Docker and how to use it for web scraping at scale.](https://scrapfly.io/blog/posts/intro-to-web-scraping-using-selenium-grid)



## Can You Use Puppeteer With Python?

Not really. Puppeteer is a javascript library that runs in Node.js and there's no official Python version of it. There is a community port called pyppeteer, however it hasn't been updated since version 2.0.0 released on February 18, 2024 and its [PyPI page](https://pypi.org/project/pyppeteer/) now says the project is unmaintained and recommends Playwright for Python instead.

Since pyppeteer is no longer maintained and Chrome's DevTools Protocol changes with new releases, we must choose a different tool for browser scraping in Python. There are several options, but the two main ones are Selenium and Playwright:

- Selenium - the official Python client maintained by the Selenium team itself.
- Playwright - [Playwright for Python](https://scrapfly.io/blog/posts/web-scraping-with-playwright-and-python) has an API very similar to Puppeteer's.
- Puppeteer itself - we can still run Puppeteer in NodeJS and call it from Python, though then our project has to deal with two languages.

Selenium is also not limited to Python - once we learn its API, we can apply this knowledge in Java, C#, Ruby and JavaScript too, as all of these clients are developed in the same project. Puppeteer on the other hand is javascript only, though it also works with TypeScript as type definitions come with the package.

[Playwright vs SeleniumExplore the key differences between Playwright vs Selenium in terms of performance, web scraping, and automation testing for modern web applications.](https://scrapfly.io/blog/posts/playwright-vs-selenium)



## Which Browsers Do They Support, and What Does Setup Look Like?

Selenium is capable of automating different browsers like Chrome, Firefox, Edge and even Safari, while Puppeteer works with Chrome and Firefox. Both tools can also download the browser parts they need - Puppeteer through an install script and Selenium through Selenium Manager on the first run.

Puppeteer is often described as Chrome only, however that's no longer the case - let's take a look!

According to the [Puppeteer supported browsers](https://pptr.dev/supported-browsers) page, Puppeteer has worked with Chrome for Testing since v20 and with stable Firefox since v23. When we install Puppeteer it also downloads the matching Chrome for Testing build (156.0.8078.47 for Puppeteer 25.16.0). Alternatively, we can use any other Chrome instance by changing the `executablePath` option, and if we want the old headless binary we can enable it through `headless: 'shell'`.

This download is done through an npm install script, however newer npm versions don't like running these. For example, if we run `npm install puppeteer` with npm 11.17 we'll see this warning:

text

```text
npm warn allow-scripts 1 package has install scripts not yet covered by allowScripts:
npm warn allow-scripts   puppeteer@25.16.0 (postinstall: node install.mjs)
```



Fortunately, this is only a warning and the script ran anyway, so Chrome for Testing 156.0.8078.47 still ended up in our empty cache. Unfortunately, npm 12 [stops running unapproved install scripts](https://github.blog/changelog/2026-06-24-upcoming-breaking-changes-for-npm-v12/) altogether, so there we'll have to approve it first with `npm approve-scripts puppeteer` or run `npx puppeteer browsers install chrome` ourselves.

For Selenium, there's [Selenium Manager](https://www.selenium.dev/documentation/selenium_manager/) - a small helper tool that comes with Selenium since version 4.6 and finds or downloads the right driver for our browser automatically. Because of this the first launch is a bit slower - with an empty cache it took us 3.3 seconds instead of the usual 680 ms. Since 4.11 it can even download Chrome for Testing when no browser is installed.

That being said, Selenium Manager is not without its faults, and one issue is that it still uses any chromedriver it finds on the PATH - even an outdated one. For example, let's put an old ChromeDriver 155 on the PATH next to our Chrome 156 and start Selenium. We'll get this warning, however the browser will still start:

text

```text
The chromedriver version (155.0.8059.79) detected in PATH at .../stale-bin/chromedriver might not be compatible with the detected chrome version (156.0.8078.80); currently, chromedriver 156.0.8078.80 is recommended for chrome 156.*, so it is advised to delete the driver in PATH and retry
```



Selenium just keeps going with the old driver, and that can break whenever Chrome changes something the driver relies on! So, it's best to delete old drivers from the PATH and let Selenium Manager resolve them. For the related error when no driver is found at all, see our [ChromeDriver not in PATH](https://scrapfly.io/blog/answers/selenium-chromedriver-in-path) answer.

[What is a Headless Browser? Top 5 Headless Browser ToolsQuick overview of new emerging tech of browser automation - what exactly are these tools and how are they used in web scraping?](https://scrapfly.io/blog/posts/what-is-a-headless-browser)



## The Same Scraper in Puppeteer and Selenium

Let's put both tools to work with the same paginated scraper. Both scripts are pretty short - 21 lines for Puppeteer and 26 for Selenium - and the main differences are async code in Puppeteer and explicit waits in Selenium.

For our example project we'll be scraping the first 5 pages of `web-scraping.dev/products`. For this we'll be collecting product title, price and URL from every page. Let's start with Puppeteer:

javascript

```javascript
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  const products = [];

  for (let i = 1; i <= 5; i++) {
    await page.goto(`https://web-scraping.dev/products?page=${i}`, { waitUntil: 'domcontentloaded' });
    const rows = await page.$$eval('.product', items => items.map(item => ({
      title: item.querySelector('h3').innerText,
      price: item.querySelector('.price').innerText,
      url: item.querySelector('h3 a').href,
    })));
    products.push(...rows);
  }

  console.log(`scraped ${products.length} products`);
  console.log(products[0]);
  await browser.close();
})();
```



In the example above, we used `page.$$eval` to run our mapping function inside the page, which sends back plain objects. This means each page costs us just one call! If we run it we'll see:

text

```text
scraped 25 products
{
  title: 'Box of Chocolate Candy',
  price: '24.99',
  url: 'https://web-scraping.dev/product/1'
}
```



Now let's do the same thing in Selenium:

python

```python
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)
products = []

for i in range(1, 6):
    driver.get(f"https://web-scraping.dev/products?page={i}")
    WebDriverWait(driver, 10).until(
        EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".product"))
    )
    products += driver.execute_script("""
        return [...document.querySelectorAll('.product')].map(item => ({
            title: item.querySelector('h3').innerText,
            price: item.querySelector('.price').innerText,
            url: item.querySelector('h3 a').href,
        }));
    """)

print(f"scraped {len(products)} products")
print(products[0])
driver.quit()
```



Here, we are using a special `WebDriverWait` object which blocks our program until a specific condition is met. In this case, our condition is the presence of product elements, and once they're loaded we read all of them with the same bulk `execute_script` trick from the speed section. And just like before, we get the same data:

text

```text
scraped 25 products
{'price': '24.99', 'title': 'Box of Chocolate Candy', 'url': 'https://web-scraping.dev/product/1'}
```



Note that product names on our practice website aren't unique - the same names show up on different pages, and out of our 25 rows only 12 had a unique title while all 25 had a unique URL. In other words, what if we remove duplicates by title? We'd lose real products! That's why it's best to rely on the product URL instead.

Both tools have many ways of checking whether the page has loaded, however the most reliable one is to wait for an element to be present in the page - in Puppeteer that's `page.waitForSelector()` and in Selenium it's the `WebDriverWait` we used above. For more, see [how to wait for a page to load in Puppeteer](https://scrapfly.io/blog/answers/how-to-wait-for-page-to-load-in-puppeteer) and [how to wait for a page to load in Selenium](https://scrapfly.io/blog/answers/how-to-wait-for-page-to-load-in-selenium).



Scrapfly

#### Need a cloud browser for scraping?

Run headless browsers at scale with Scrapfly Cloud Browser — no infrastructure to manage.

[Try Free →](https://scrapfly.io/register)## How Do You Block Images and Intercept Requests in Each?

When web-scraping we often don't need to load images, which is a slow and intensive process. Puppeteer has request interception built right into its API, while Selenium can do it on Chrome through a DevTools command or through WebDriver BiDi on newer setups.

In Puppeteer, we can enable [request interception](https://pptr.dev/guides/network-interception) and decide what to do with every request:

javascript

```javascript
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  await page.setRequestInterception(true);

  let blocked = 0;
  page.on('request', request => {
    if (request.resourceType() === 'image') {
      blocked++;
      request.abort();
    } else {
      request.continue();
    }
  });

  await page.goto('https://web-scraping.dev/products', { waitUntil: 'networkidle0' });
  const titles = await page.$$eval('.product h3', els => els.map(e => e.innerText));
  console.log(`blocked ${blocked} images, scraped ${titles.length} titles`);
  await browser.close();
})();
```



In the example above, we enabled request interception and attached a handler to every request. Then, for each request we have an option to either abort it if it's an image or let it continue. If we run it, we'll see that the page still gave us all five titles:

text

```text
blocked 7 images, scraped 5 titles
```



In Selenium, we can instruct Chrome browser to block images through the `execute_cdp_cmd` method which sends DevTools commands:

python

```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": ["*.webp", "*.png", "*.jpg", "*.jpeg", "*.gif", "*.svg"]
})

driver.get("https://web-scraping.dev/products")
images = driver.find_elements(By.CSS_SELECTOR, ".product img")
loaded = sum(1 for img in images if driver.execute_script("return arguments[0].naturalWidth", img) > 0)
titles = [el.text for el in driver.find_elements(By.CSS_SELECTOR, ".product h3")]
print(f"{loaded} of {len(images)} product images loaded, scraped {len(titles)} titles")
driver.quit()
```



Here, we are using the `Network.setBlockedURLs` command which blocks requests by URL patterns instead of resource types, so we have to list all image extensions ourselves. If we run it we'll get:

text

```text
0 of 5 product images loaded, scraped 5 titles
```



No images loaded and nothing broke!

That being said, this only works on Chromium browsers (Chrome and Edge). Selenium's cross-browser route is [WebDriver BiDi](https://www.selenium.dev/documentation/webdriver/bidi/), enabled with the `enable_bidi` option and used through `driver.network.add_request_handler()`.

Request interception is not only useful for blocking though. When a browser loads a web page it also makes many background requests, and these often contain the same data in cleaner JSON format! We cover that in [capturing XHR requests in Puppeteer](https://scrapfly.io/blog/answers/how-to-capture-xhr-requests-puppeteer) and [capturing XHR requests in Selenium](https://scrapfly.io/blog/answers/how-to-capture-xhr-requests-selenium).

For more blocking patterns, see [blocking resources in Puppeteer](https://scrapfly.io/blog/answers/how-to-block-resources-in-puppeteer) and [blocking resources in Selenium](https://scrapfly.io/blog/answers/how-to-block-resources-in-selenium).

[Web Scraping Background Requests with Headless BrowsersIn this tutorial we'll be taking a look at a rather new and popular web scraping technique - capturing background requests using headless browsers.](https://scrapfly.io/blog/posts/web-scraping-background-requests-with-headless-browsers-and-python)



## Does Puppeteer or Selenium Get Blocked Less?

First, let's take a look at what both of these tools look like by default. The most commonly known leak when it comes to browser automation tools like Selenium or Puppeteer is the `navigator.webdriver` value, which was set to `true` for both of them in our headless Chrome 156 runs. Not only that, but both tools also reported `HeadlessChrome/156.0.0.0` in their User-Agent string!

To see what else gives these tools away, we've run both of them against [bot.sannysoft.com](https://bot.sannysoft.com) - a popular test page that checks the most common headless browser signals. Note that the site wouldn't load from our test machine, so we've saved a copy of the page through a VPN and served it locally (by then our Chrome had also updated to version 157). Both tools failed the exact same 4 checks in headless mode - User Agent, WebDriver, HEADCHR\_UA and CHR\_MEMORY - and only the WebDriver check once we turned headless mode off:



In other words, neither tool is stealthier than the other out of the box - both drive the same Chrome and show the same signals.

Unfortunately, a test page like this only checks the basics. In other words, a real website protected by anti-bot services could still block both tools, and we haven't tested which one gets blocked more often. To change the fingerprint itself, see the guide for each of these tools:

- [Puppeteer stealth plugin](https://scrapfly.io/blog/posts/puppeteer-stealth-complete-guide) - patches common headless signals in Puppeteer
- [undetected-chromedriver](https://scrapfly.io/blog/posts/web-scraping-without-blocking-using-undetected-chromedriver) - a patched ChromeDriver for Selenium
- [SeleniumBase](https://scrapfly.io/blog/posts/guide-to-seleniumbase-better-selenium) - Selenium with a UC mode built on the same idea

[How Anti-Bot Detection WorksDiscover how anti-bot systems work by assigning a 'trust score' to every request. This guide breaks down the 5 key detection layers, from TLS fingerprinting to behavioral analysis.](https://scrapfly.io/blog/posts/how-anti-bot-detection-works)

[How Browser Fingerprinting Works and How to Defend Against ItLearn how browser fingerprinting works, from canvas to WebGPU, test your fingerprint with free tools, and apply developer-focused anti-detection strategies in your web scrapers.](https://scrapfly.io/blog/posts/how-browser-fingerprinting-works)



## Which Should You Choose for Web Scraping?

We've covered both tools from setup to blocking - so which one should we stick with? Well, it mostly depends on the project we're working on. If our project is in javascript and runs on Chrome then Puppeteer is a great fit, especially when we read lots of data from each page. However, for Python, Java or C# projects, for scraping in Safari or Edge, or if we already have a Selenium Grid running, Selenium is the better choice.

So, for most of us the choice comes down to which language and browser we're using rather than speed:

| Your situation | Pick |
|---|---|
| Node.js or TypeScript project | Puppeteer |
| Python, Java, C# or Ruby project | Selenium |
| Must test or scrape in Safari | Selenium |
| Many small element reads per page | Puppeteer (or Selenium with bulk `execute_script`) |
| Request interception and event listeners | Puppeteer |
| Existing Selenium Grid or test suite | Selenium |

That being said, there's one more tool worth mentioning. Playwright is available in Python, Java and .NET as well as javascript, comes with its own browser builds and has an API very similar to Puppeteer's. For more, see our [Playwright vs Selenium](https://scrapfly.io/blog/posts/playwright-vs-selenium) and [Puppeteer vs Playwright](https://scrapfly.io/blog/posts/puppeteer-vs-playwright) comparisons.

Finally, what if we need to run more browsers than one machine can handle? For Selenium that's usually Selenium Grid, which spreads WebDriver sessions across many machines. Puppeteer, on the other hand, can connect to a remote browser through a WebSocket using `puppeteer.connect()` - and this is how we can use Puppeteer with [Cloud Browser](https://scrapfly.io/docs/cloud-browser-api/puppeteer):

javascript

```javascript
const puppeteer = require('puppeteer');

const params = new URLSearchParams({
  api_key: process.env.SCRAPFLY_KEY,
  proxy_pool: 'datacenter',
  target_url: 'https://web-scraping.dev/products',
});

(async () => {
  const start = Date.now();
  const browser = await puppeteer.connect({
    browserWSEndpoint: `wss://browser.scrapfly.io?${params}`,
  });
  console.log(`connected in ${Date.now() - start} ms`);

  const page = await browser.newPage();
  await page.goto('https://web-scraping.dev/products', { waitUntil: 'domcontentloaded' });
  const titles = await page.$$eval('.product h3', els => els.map(e => e.innerText));
  console.log(titles);
  console.log(await page.evaluate(() => navigator.userAgent));
  await browser.close();
})();
```



As you can see, our code looks almost identical to a local scraper except for `connect()` with a WebSocket URL instead of `launch()`. If we run it, we'll see:

text

```text
connected in 3889 ms
[
  'Box of Chocolate Candy',
  'Dark Red Energy Potion',
  'Teal Energy Potion',
  'Red Energy Potion',
  'Blue Energy Potion'
]
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36
```



Note the User-Agent says `Chrome/156.0.0.0` rather than the `HeadlessChrome` our local headless runs reported!

Unfortunately, Cloud Browser only speaks the Chrome DevTools Protocol and has no WebDriver endpoint, so Selenium can't connect to it like this - for that, Scrapfly's docs suggest using Puppeteer or Playwright instead. For request-based scraping from Python, the [Web Scraping API](https://scrapfly.io/products/web-scraping-api) is another route.

[Puppeteer vs Playwright for Web ScrapingPuppeteer vs Playwright for web scraping, compared on browsers, waiting, contexts, CDP, speed, and stealth, with a decision guide by workload.](https://scrapfly.io/blog/posts/puppeteer-vs-playwright)



## Power-up with Scrapfly

There's quite a bit of work involved in keeping a Puppeteer or Selenium scraper from being detected - as we've seen, both tools report `HeadlessChrome` and `navigator.webdriver` right out of the box. The best way to deal with challenges like these is to leave the browser part to someone else!



Scrapfly's [Cloud Browser](https://scrapfly.io/products/cloud-browser-api) runs real browsers in the cloud which we can control with Puppeteer just like a local one. All we have to do is replace `puppeteer.launch()` with `puppeteer.connect()` like we did in our example above, and the rest of our code stays the same. Not only that, but every browser comes with:

- Scrapium - Scrapfly's own Chromium fork with 550+ patched source files and over 4,000 browser signals taken from real device profiles.
- Residential proxies in 190+ countries, with the browser's timezone, locale and language matched to the proxy location.
- [Persistent sessions](https://scrapfly.io/docs/cloud-browser-api/session-resume) - cookies and localStorage are kept between connections, so we can log in once and continue later.
- Session recording - each session can be replayed in the dashboard, which makes debugging a lot easier.

Since Cloud Browser speaks only the Chrome DevTools Protocol, Selenium projects can't plug into it directly. For Python scrapers, the [Web Scraping API](https://scrapfly.io/products/web-scraping-api) can [render javascript](https://scrapfly.io/docs/scrape-api/javascript-rendering) and [bypass anti-bot protection](https://scrapfly.io/docs/scrape-api/unblocker) through a simple HTTP request instead.



### Web Scraping API

Scrape any website with our powerful API. Anti-bot bypass, JavaScript rendering, and rotating proxies built-in.



[Try Web Scraping API](https://scrapfly.io/docs/scrape-api/getting-started)



## FAQ

Is Puppeteer still relevant?Yes, Puppeteer is still maintained by the Chrome Browser Automation team and each new version is pinned to a recent Chrome for Testing build. For example, Puppeteer 25.16.0 uses Chrome for Testing 156.0.8078.47 and Firefox 158.0.1 - see its [supported browsers](https://pptr.dev/supported-browsers) page. Not only that, but since v23 it also works with stable Firefox releases.







Is Python used with Selenium?Yes, Python is one of the officially supported languages of Selenium and it can be installed using the `pip install selenium` command. All Selenium clients use the same WebDriver API, so our Python examples above would look almost the same in Java, C#, Ruby or javascript.







Can Puppeteer and Selenium run headless?Yes, both of them can. Puppeteer starts the browser in headless mode by default, while for Selenium we have to pass the `--headless=new` argument to Chrome like we did in our examples. Keep in mind that headless browsers are easier to spot - for example, in our runs both tools reported `HeadlessChrome` in their User-Agent string.







Is Puppeteer better than Selenium for scraping dynamic websites?Both tools run a real web browser, so they can render javascript content equally well. However, Puppeteer makes it easier to wait for and capture background requests as it can listen to network events - the `request` handler we used for blocking images is a good starting point for that.







Can Selenium use the Chrome DevTools Protocol like Puppeteer?Yes, but only on Chrome and Edge through the `execute_cdp_cmd` method - that's what we used in our Selenium image blocking example to call `Network.setBlockedURLs`. For other browsers, Selenium uses WebDriver BiDi instead.









## Summary

In this comparison, we looked at the Puppeteer and Selenium web browser automation packages and how they compare when it comes to web scraping. We covered some common use case scenarios like waiting for content and blocking images, and explored challenges like setup, speed and avoiding being detected. We also wrote the same small example scraper in both tools which collects product data from web-scraping.dev.

By running both tools on the same Chrome browser we've seen that they scrape at almost the same speed (only 79 ms apart for 5 pages), though Selenium is a lot slower when reading many elements one by one. So, Puppeteer is a great choice for javascript projects on Chrome, while Selenium is better for Python, Java or C# projects and other browsers.

Finally, we wrapped everything up by looking at how Puppeteer can connect to Scrapfly's Cloud Browser by swapping `launch()` for `connect()`, and how Python projects can use the Web Scraping API instead!



Legal Disclaimer and PrecautionsThis tutorial covers popular web scraping techniques for education. Interacting with public servers requires diligence and respect:

- Do not scrape at rates that could damage the website.
- Do not scrape data that's not available publicly.
- Do not store PII of EU citizens protected by GDPR.
- Do not repurpose *entire* public datasets which can be illegal in some countries.

Scrapfly does not offer legal advice but these are good general rules to follow. For more you should consult a lawyer.

 

   [  Add as a preferred source ](https://google.com/preferences/source?q=scrapfly.io) Table of Contents















 

  Table of Contents

- [Key Takeaways](#key-takeaways)
- [Puppeteer vs Selenium at a Glance](#puppeteer-vs-selenium-at-a-glance)
- [How Do Puppeteer and Selenium Control the Browser?](#how-do-puppeteer-and-selenium-control-the-browser)
- [Is Puppeteer Faster Than Selenium?](#is-puppeteer-faster-than-selenium)
- [Can You Use Puppeteer With Python?](#can-you-use-puppeteer-with-python)
- [Which Browsers Do They Support, and What Does Setup Look Like?](#which-browsers-do-they-support-and-what-does-setup-look-like)
- [The Same Scraper in Puppeteer and Selenium](#the-same-scraper-in-puppeteer-and-selenium)
- [How Do You Block Images and Intercept Requests in Each?](#how-do-you-block-images-and-intercept-requests-in-each)
- [Does Puppeteer or Selenium Get Blocked Less?](#does-puppeteer-or-selenium-get-blocked-less)
- [Which Should You Choose for Web Scraping?](#which-should-you-choose-for-web-scraping)
- [Power-up with Scrapfly](#power-up-with-scrapfly)
- [FAQ](#faq)
- [Summary](#summary)
 
    Join the Newsletter  Get monthly web scraping insights 

 

  



Scale Your Web Scraping

Anti-bot bypass, browser rendering, and rotating proxies, all in one API. Start with 1,000 free credits.

  No credit card required  1,000 free API credits  Anti-bot bypass included 

 [Start Free](https://scrapfly.io/register) [View Docs](https://scrapfly.io/docs/onboarding) 

 Not ready? Get our newsletter instead. 

 

 ## Related Articles

 [     

 python scaling 

### Web Scraping Speed: Processes, Threads and Async

Scaling web scrapers can be difficult - in this article we'll go over the core principles like subprocesses, threads and...

 

 ](https://scrapfly.io/blog/posts/web-scraping-speed) [     

 python nodejs 

### How to use Headless Chrome Extensions for Web Scraping

In this article, we'll explore different useful Chrome extensions for web scraping. We'll also explain how to install Ch...

 

 ](https://scrapfly.io/blog/posts/how-to-use-browser-extensions-with-playwright-puppeteer-and-selenium) [     

 nodejs headless-browser 

### How to Web Scrape with Puppeteer and NodeJS in 2026

Introduction to using Puppeteer in Nodejs for web scraping dynamic web pages and web apps. Tips and tricks, best practic...

 

 ](https://scrapfly.io/blog/posts/web-scraping-with-puppeteer-and-nodejs) 

  ## Related Questions

- [ Q How to block resources in Puppeteer? ](https://scrapfly.io/blog/answers/how-to-block-resources-in-puppeteer)
- [ Q How to save and load cookies in Puppeteer? ](https://scrapfly.io/blog/answers/how-to-save-and-load-cookies-in-puppeteer)
 
  



   



 Run headless browsers at scale, **1,000 free credits** [Start Free](https://scrapfly.io/register)