Node.js Undetected Fingerprint Browser: Puppeteer & Playwright
Expert in Web Scraping Technologies
TL;DR:
- In Node.js, the browser is the thing that gets you detected β not your code. Puppeteer and Playwright both drive Chromium over the DevTools Protocol, and both connect to a remote browser with one URL, so the fix is changing where the browser runs, not rewriting the script.
- A local Node.js browser leaks automation on three axes at once: the
navigator.webdriverflag, a headless-default fingerprint, and your own exit IP. Detectors score all three together, which is why patching one at a time never quite holds. - One endpoint serves both libraries.
wss://browser.scrapeless.com/api/v2/browseris a CDP endpoint βpuppeteer.connect({ browserWSEndpoint })andchromium.connectOverCDP()both attach to it and get real Chromium, a clean per-session fingerprint, and residential egress in 195+ countries. - Write the connection once, reuse it everywhere. A single helper that builds the endpoint keeps every script β Puppeteer or Playwright β pointed at the same managed session.
- Verified live: both a Puppeteer and a Playwright connection read
navigator.webdriverasfalseand returned the target page's real title, and the Puppeteer run reported a US residential exit IP. - Free to start. New Scrapeless accounts include free Scraping Browser runtime β sign up at app.scrapeless.com.
Introduction: the browser is the tell, not the script
A Node.js scraping stack usually starts local: install Puppeteer or Playwright, launch Chromium, drive the page. It works until the target starts scoring the browser. Then the same three signals show up regardless of which library you picked β the script announces automation through the navigator.webdriver property, the headless build ships default fonts and canvas behavior, and the traffic exits from an IP reputation services already know.
Those are browser-level and network-level problems, not logic problems. Both Puppeteer and Playwright already know how to attach to a browser they didn't launch β over the Chrome DevTools Protocol β and the Scrapeless Scraping Browser is exactly that: a CDP endpoint reached over one WebSocket URL. Point either library at it and the fingerprint, behavior, and exit IP move server-side while your Node.js code stays put.
What you can do with it
- Keep your library β Puppeteer and Playwright both attach to the same endpoint; no rewrite.
- Run against anti-bot-protected sites β challenges clear during render, so navigation reaches content.
- Localize the session β one
proxyCountryvalue chooses the residential exit region. - Send a consistent fingerprint β a
fingerprintobject keeps user agent, platform, screen, and timezone coherent. - Persist state β a
profileIdcarries cookies and local storage across runs. - Scale off your laptop β sessions run in the cloud, so batches aren't bound to one local Chromium.
Why Scrapeless Scraping Browser
The Scrapeless Scraping Browser is a customizable, anti-detection cloud browser designed for web crawlers and AI agents. For a Node.js stack specifically, it brings:
- Real self-developed Chromium that runs page JavaScript exactly like Chrome, so SPAs and challenges resolve.
- A consistent per-session fingerprint β no
navigator.webdrivertell, no headless defaults leaking through. - Residential egress in 195+ countries, selected per session with one
proxyCountryvalue. - One CDP surface for both libraries β Puppeteer and Playwright connect to the same WebSocket endpoint.
- Session persistence through
profileId, so an authenticated flow keeps its state.
Get your API key on the free plan at app.scrapeless.com.
Prerequisites
- Node.js 18 or newer
puppeteer-coreand/orplaywright-core(both skip the local browser download β you connect to a remote one)- A Scrapeless account and API key β sign up at app.scrapeless.com
Full connection details live in the Scraping Browser documentation.
Install
Install whichever library your project already uses β or both.
bash
npm install puppeteer-core playwright-core
export SCRAPELESS_API_KEY=your_api_token_here # free key at https://app.scrapeless.com
Write the connection once
The endpoint is the same for both libraries, so build it in one place. Every option β proxyCountry, fingerprint, profileId β is a query parameter.
javascript
// scrapeless.js β one endpoint builder for the whole project
import { URLSearchParams } from "node:url";
export function scrapelessEndpoint(extra = {}) {
const params = new URLSearchParams({
token: process.env.SCRAPELESS_API_KEY,
sessionTTL: "180", // seconds
proxyCountry: "US",
...extra,
});
return `wss://browser.scrapeless.com/api/v2/browser?${params}`;
}
Path A β Puppeteer
Puppeteer attaches with puppeteer.connect(). The W3C WebDriver specification requires conforming automation to expose the webdriver flag; the cloud browser does not carry it.
javascript
import puppeteer from "puppeteer-core";
import { URLSearchParams } from "node:url";
const params = new URLSearchParams({
token: process.env.SCRAPELESS_API_KEY,
sessionTTL: "180",
proxyCountry: "US",
});
const endpoint = `wss://browser.scrapeless.com/api/v2/browser?${params}`;
const browser = await puppeteer.connect({ browserWSEndpoint: endpoint });
const page = await browser.newPage();
await page.goto("https://www.scrapeless.com/en", { waitUntil: "domcontentloaded", timeout: 60000 });
console.log("puppeteer title:", await page.title());
console.log("puppeteer navigator.webdriver:", await page.evaluate(() => navigator.webdriver));
await browser.close();
A live run printed puppeteer title: Effortless Web Scraping Toolkit - Scrapeless and puppeteer navigator.webdriver: false, on a US residential exit IP.
Get your API key on the free plan: app.scrapeless.com
Path B β Playwright
Playwright attaches with chromium.connectOverCDP() to the same endpoint. The code that follows is standard Playwright.
javascript
import { chromium } from "playwright-core";
import { URLSearchParams } from "node:url";
const params = new URLSearchParams({
token: process.env.SCRAPELESS_API_KEY,
sessionTTL: "180",
proxyCountry: "US",
});
const endpoint = `wss://browser.scrapeless.com/api/v2/browser?${params}`;
const browser = await chromium.connectOverCDP(endpoint);
const page = await browser.newPage();
await page.goto("https://www.scrapeless.com/en", { waitUntil: "domcontentloaded", timeout: 60000 });
console.log("playwright title:", await page.title());
console.log("playwright navigator.webdriver:", await page.evaluate(() => navigator.webdriver));
await browser.close();
A live run printed playwright title: Effortless Web Scraping Toolkit - Scrapeless and playwright navigator.webdriver: false.
Choosing between them
The endpoint is identical, so pick the library on its own merits, not on which one clears detection β the cloud browser handles that for both:
- Already on Puppeteer? Change
puppeteer.launch()topuppeteer.connect({ browserWSEndpoint }). Nothing else moves. - Already on Playwright? Change
chromium.launch()tochromium.connectOverCDP(). Locators and auto-waiting are unchanged. - Starting fresh? Playwright's locators and auto-waiting are ergonomic for complex flows; Puppeteer is lean when you mostly navigate and read. Either connects to the same session.
Where local Node.js browsers stop
Running Chromium on your own machine is fine for open pages. The friction starts where the target scores the browser: a challenge interstitial that stalls on a datacenter IP, a page that fingerprints the headless defaults, or a login wall that wants a stable identity across visits. Those are fingerprint, behavior, and IP problems β the three the cloud browser handles server-side β and they hit Puppeteer and Playwright the same way. Connecting to Scrapeless hands those cases to a managed session while your Node.js code stays identical.
What you get back
Both paths return a normal browser object for their library β Puppeteer pages, Playwright locators β and behave like any local session. A few honest observations:
navigator.webdriverisfalseon both, so the first check a site runs reads like a real Chrome.sessionTTLis in seconds here β set it to cover the whole flow; the session closes when it lapses.- The exit IP matches
proxyCountryβ geo-gated pages resolve as a local visitor sees them. - Warm the session for stubborn pages β load the site's homepage first before the protected page, so the behavioral surface reads like a normal visit.
Conclusion: one endpoint, either library
In Node.js the detection surface is the browser, so the fix is to move the browser β not rewrite the scraper. Build the endpoint once, then puppeteer.connect() or chromium.connectOverCDP() against it and keep the rest of your code. Pin proxyCountry to a residential region, pass a coherent fingerprint, and reuse a profileId for authenticated flows. For running the same cloud browser from Python instead, see the Scrapling production-scraper guide, and compare plans on the Scrapeless pricing page.
Ready to Build Your Node.js Scraping Pipeline?
Join our community to claim a free plan and connect with developers building Node.js scraping pipelines: Discord Β· Telegram.
Sign up at app.scrapeless.com for free Scraping Browser runtime and point Puppeteer or Playwright at the cloud browser your targets can't fingerprint.
FAQ
Q: Do I have to pick between Puppeteer and Playwright?
No. Both attach to the same Scrapeless endpoint β puppeteer.connect({ browserWSEndpoint }) or chromium.connectOverCDP(). Use whichever your project already runs, or both.
Q: Do I change my scraping logic?
No. You change how the browser is obtained; navigation, selectors/locators, and evaluation stay the same.
Q: Why -core packages?
puppeteer-core and playwright-core skip the bundled browser download because you connect to the cloud browser instead of launching a local one.
Q: Do I need a proxy?
No. Residential egress is built in β set proxyCountry to a region or ANY.
Q: Is web scraping with Node.js legal?
Accessing publicly available data is generally permitted, but the rules vary by jurisdiction and site. Review the target's terms of service, respect robots directives, and consult counsel for anything sensitive.
At Scrapeless, we only access publicly available data while strictly complying with applicable laws, regulations, and website privacy policies. The content in this blog is for demonstration purposes only and does not involve any illegal or infringing activities. We make no guarantees and disclaim all liability for the use of information from this blog or third-party links. Before engaging in any scraping activities, consult your legal advisor and review the target website's terms of service or obtain the necessary permissions.



