How to Make Puppeteer Avoid Bot Detection
To make Puppeteer avoid bot detection, you need to address every signal that anti-bot systems check: the navigator.webdriver flag, headless Chrome signatures, CDP protocol fingerprints, canvas and WebGL rendering anomalies, and behavioral patterns like instant clicks and linear mouse paths. Below, we break down nine field-tested techniques — each with working Node.js code — that neutralize these detection vectors one by one, from the quick-win stealth plugin to connecting Puppeteer to a fully isolated antidetect browser profile.

Why Standard Puppeteer Gets Detected Instantly
Out of the box, a Puppeteer-controlled Chrome instance leaks dozens of automation signals. Anti-bot platforms like Cloudflare Turnstile, DataDome, PerimeterX, and Akamai Bot Manager run JavaScript challenges on every page load that check for these exact tells. Understanding what they look for is the first step toward beating them.
Detection Vector 1: The navigator.webdriver Flag
When Chrome launches under automation, it sets navigator.webdriver to true. This single property is the lowest-hanging fruit for any anti-bot script — a one-line check that catches unmodified Puppeteer sessions every time.
// What the anti-bot script checks
if (navigator.webdriver === true) {
// Flag as bot — block or serve CAPTCHA
}
Detection Vector 2: Headless Chrome Signals
Even in “new headless” mode (--headless=new), Chrome exposes subtle differences: missing plugin arrays, a chrome.runtime object that behaves differently, absent Notification.permission, and User-Agent strings containing “HeadlessChrome.” Sophisticated detectors stack half a dozen of these checks together.
Detection Vector 3: Chrome DevTools Protocol Fingerprint
Puppeteer communicates with Chrome over the Chrome DevTools Protocol (CDP). This connection itself is detectable — the browser exposes a /json/version endpoint on a debug port, and certain JavaScript runtime properties like window.cdc_adoQpoasnfa76pfcZLmcfl (the CDP binding variable) betray automation. For a deeper look at how browsers leak identity data, see our guide on browser fingerprint explained.
Detection Vector 4: Canvas and WebGL Fingerprinting
Anti-bot systems render invisible canvas elements and WebGL scenes, then hash the pixel output. Headless Chrome produces deterministic, GPU-less renders that hash identically across all headless instances — an instant red flag. Real browsers show natural variation based on GPU hardware, driver version, and OS rendering differences. Our technical breakdown of WebGL fingerprinting covers the math behind these hashes.
Detection Vector 5: Behavioral Analysis
Modern anti-bot platforms go beyond static fingerprints. They track mouse movement entropy (real humans produce curved, noisy paths — bots move in straight lines), typing cadence, scroll velocity, the time between page load and first interaction, and even how a user navigates between pages. A script that instantly clicks a button 200ms after page load is trivially identifiable.
Method 1: puppeteer-extra-plugin-stealth
The stealth plugin is the fastest way to patch the most common detection vectors. It wraps Puppeteer in a set of evasion modules that fix navigator.webdriver, spoof the Chrome runtime, mask headless signals, and more — all without modifying Chrome’s binary.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
// Apply all stealth evasions
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({
headless: false, // 'new' headless still leaks some signals
args: [
'--no-sandbox',
'--disable-blink-features=AutomationControlled',
'--disable-infobars',
],
});
const page = await browser.newPage();
// Verify stealth is working
await page.goto('https://bot.sannysoft.com');
await page.screenshot({ path: 'stealth-check.png' });
console.log('Stealth test complete — check stealth-check.png');
await browser.close();
})();
The stealth plugin handles roughly 10 evasion modules, including chrome.runtime, navigator.permissions, iframe.contentWindow, and codec enumeration. It is a strong baseline but not sufficient on its own against advanced systems like Cloudflare Turnstile or DataDome — you need to layer additional techniques on top.
Method 2: Custom User-Agent and Client Hints
Anti-bot scripts compare the User-Agent header against the JavaScript-side navigator.userAgent and the newer navigator.userAgentData (User-Agent Client Hints). A mismatch between any of these is an immediate detection signal. You need to set all three consistently.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
// Define a realistic User-Agent
const userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
// Set HTTP-level User-Agent
await page.setUserAgent(userAgent);
// Override navigator.userAgentData to match
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'userAgentData', {
get: () => ({
brands: [
{ brand: 'Chromium', version: '126' },
{ brand: 'Google Chrome', version: '126' },
{ brand: 'Not-A.Brand', version: '99' },
],
mobile: false,
platform: 'Windows',
getHighEntropyValues: async (hints) => ({
architecture: 'x86',
bitness: '64',
fullVersionList: [
{ brand: 'Chromium', version: '126.0.6478.127' },
{ brand: 'Google Chrome', version: '126.0.6478.127' },
],
mobile: false,
model: '',
platform: 'Windows',
platformVersion: '15.0.0',
uaFullVersion: '126.0.6478.127',
}),
}),
});
});
await page.goto('https://httpbin.org/user-agent');
const body = await page.evaluate(() => document.body.innerText);
console.log('Server sees:', body);
await browser.close();
})();
Keep a pool of 5-10 recent, valid User-Agent strings and rotate them between sessions. Stale User-Agents (e.g., Chrome 90 in 2026) are themselves a detection signal.
Method 3: Viewport and Screen Property Randomization
Default Puppeteer launches with an 800×600 viewport — a resolution that virtually no real user has. Anti-bot scripts check window.screen, window.innerWidth, window.outerWidth, and device pixel ratio. Randomizing these to common real-world values is critical.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
// Common real-world viewport configurations
const viewports = [
{ width: 1920, height: 1080, deviceScaleFactor: 1 },
{ width: 1366, height: 768, deviceScaleFactor: 1 },
{ width: 1536, height: 864, deviceScaleFactor: 1.25 },
{ width: 1440, height: 900, deviceScaleFactor: 2 },
{ width: 2560, height: 1440, deviceScaleFactor: 1 },
];
function getRandomViewport() {
return viewports[Math.floor(Math.random() * viewports.length)];
}
(async () => {
const vp = getRandomViewport();
const browser = await puppeteer.launch({
headless: false,
args: [`--window-size=${vp.width},${vp.height}`],
});
const page = await browser.newPage();
await page.setViewport(vp);
// Spoof screen properties to match viewport
await page.evaluateOnNewDocument((viewport) => {
Object.defineProperty(screen, 'width', { get: () => viewport.width });
Object.defineProperty(screen, 'height', { get: () => viewport.height });
Object.defineProperty(screen, 'availWidth', { get: () => viewport.width });
Object.defineProperty(screen, 'availHeight', { get: () => viewport.height - 40 });
Object.defineProperty(screen, 'colorDepth', { get: () => 24 });
Object.defineProperty(screen, 'pixelDepth', { get: () => 24 });
}, vp);
await page.goto('https://whatismyviewport.com');
console.log(`Using viewport: ${vp.width}x${vp.height} @ ${vp.deviceScaleFactor}x`);
await browser.close();
})();
Method 4: Realistic Mouse Movement Simulation
Jumping the cursor directly to a button and clicking is a dead giveaway. Real users produce curved, slightly erratic mouse paths with variable speed. The Bézier curve approach simulates this convincingly.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
/**
* Move mouse along a Bézier curve to target coordinates.
* Produces natural-looking curved paths with speed variation.
*/
async function humanMove(page, targetX, targetY, steps = 25) {
const start = await page.evaluate(() => ({
x: window.mouseX || Math.random() * 200,
y: window.mouseY || Math.random() * 200,
}));
// Generate two random control points for a cubic Bézier
const cp1x = start.x + (targetX - start.x) * (0.2 + Math.random() * 0.3);
const cp1y = start.y + (Math.random() - 0.5) * 200;
const cp2x = start.x + (targetX - start.x) * (0.6 + Math.random() * 0.3);
const cp2y = targetY + (Math.random() - 0.5) * 150;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const inv = 1 - t;
// Cubic Bézier formula
const x =
inv ** 3 * start.x +
3 * inv ** 2 * t * cp1x +
3 * inv * t ** 2 * cp2x +
t ** 3 * targetX;
const y =
inv ** 3 * start.y +
3 * inv ** 2 * t * cp1y +
3 * inv * t ** 2 * cp2y +
t ** 3 * targetY;
await page.mouse.move(x, y);
// Variable delay — slower at start and end (ease in/out)
const speed = 8 + Math.random() * 12 + Math.sin(t * Math.PI) * 10;
await new Promise((r) => setTimeout(r, speed));
}
}
/**
* Click an element with human-like mouse movement first.
*/
async function humanClick(page, selector) {
const el = await page.$(selector);
if (!el) throw new Error(`Element not found: ${selector}`);
const box = await el.boundingBox();
// Click a random point within the element, not dead center
const targetX = box.x + box.width * (0.3 + Math.random() * 0.4);
const targetY = box.y + box.height * (0.3 + Math.random() * 0.4);
await humanMove(page, targetX, targetY);
// Brief pause before clicking (humans hesitate)
await new Promise((r) => setTimeout(r, 50 + Math.random() * 150));
await page.mouse.click(targetX, targetY);
}
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://example.com');
await humanClick(page, 'a');
await browser.close();
})();
This technique is especially important on sites using behavioral biometrics (e.g., PerimeterX, HUMAN Security). Pair it with random scroll events and realistic typing delays for full behavioral stealth.
Method 5: Proxy Rotation with Session Persistence
IP reputation is a major detection factor. Datacenter IPs are flagged immediately by most anti-bot systems. Residential or mobile proxies with per-session sticky IP assignment look far more natural. To understand the broader picture of how sites detect automated traffic, check our bypass anti-bot detection guide.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
// Residential proxy pool (replace with your provider's endpoints)
const proxies = [
'http://user:[email protected]:12321',
'http://user:[email protected]:7000',
'http://user:[email protected]:7777',
];
function getRandomProxy() {
return proxies[Math.floor(Math.random() * proxies.length)];
}
(async () => {
const proxyUrl = getRandomProxy();
const browser = await puppeteer.launch({
headless: false,
args: [`--proxy-server=${proxyUrl}`],
});
const page = await browser.newPage();
// Authenticate with the proxy if using user:pass format
await page.authenticate({
username: 'user',
password: 'pass',
});
await page.goto('https://httpbin.org/ip');
const body = await page.evaluate(() => document.body.innerText);
console.log('Exit IP:', body);
await browser.close();
})();
Rotate IPs between sessions but keep the same IP within a single session — switching mid-session is itself suspicious. Use country-targeted proxies that match the locale and timezone you have set in the browser.
Method 6: Timezone, Locale, and Geolocation Consistency
A browser claiming a US User-Agent but reporting Intl.DateTimeFormat().resolvedOptions().timeZone as “Europe/Berlin” will trigger fingerprint inconsistency flags. Every dimension needs to match.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({
headless: false,
args: ['--lang=en-US'],
});
const context = browser.defaultBrowserContext();
// Grant geolocation permission
await context.overridePermissions('https://example.com', ['geolocation']);
const page = await browser.newPage();
// Set timezone to match the proxy location
await page.emulateTimezone('America/New_York');
// Override geolocation (New York area)
await page.setGeolocation({
latitude: 40.7128,
longitude: -74.006,
accuracy: 100,
});
// Set language preferences
await page.setExtraHTTPHeaders({
'Accept-Language': 'en-US,en;q=0.9',
});
// Spoof Intl to match
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'language', { get: () => 'en-US' });
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en'],
});
});
await page.goto('https://browserleaks.com/geo');
console.log('Geolocation and timezone set to US East');
await browser.close();
})();
Method 7: WebGL and Canvas Fingerprint Spoofing
Spoofing canvas and WebGL at the Puppeteer level requires intercepting the rendering APIs and injecting controlled noise. This makes each session produce a unique — but plausible — fingerprint hash instead of the identical hash that headless instances share.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.evaluateOnNewDocument(() => {
// --- Canvas fingerprint noise ---
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function (type) {
if (type === 'image/png' || type === undefined) {
const ctx = this.getContext('2d');
if (ctx) {
const imageData = ctx.getImageData(0, 0, this.width, this.height);
// Add subtle random noise to pixel data
for (let i = 0; i < imageData.data.length; i += 4) {
imageData.data[i] += Math.floor(Math.random() * 3) - 1; // R
imageData.data[i + 1] += Math.floor(Math.random() * 3) - 1; // G
imageData.data[i + 2] += Math.floor(Math.random() * 3) - 1; // B
}
ctx.putImageData(imageData, 0, 0);
}
}
return originalToDataURL.apply(this, arguments);
};
// --- WebGL renderer/vendor spoofing ---
const getParamOriginal = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function (param) {
// UNMASKED_VENDOR_WEBGL
if (param === 37445) return 'Intel Inc.';
// UNMASKED_RENDERER_WEBGL
if (param === 37446) return 'Intel Iris OpenGL Engine';
return getParamOriginal.call(this, param);
};
const getParam2 = WebGL2RenderingContext.prototype.getParameter;
WebGL2RenderingContext.prototype.getParameter = function (param) {
if (param === 37445) return 'Intel Inc.';
if (param === 37446) return 'Intel Iris OpenGL Engine';
return getParam2.call(this, param);
};
});
await page.goto('https://browserleaks.com/canvas');
console.log('Canvas and WebGL fingerprints spoofed');
await browser.close();
})();
This approach has limits: aggressive noise can make the canvas output look implausible if the variance is too high, and some detectors hash WebGL shader compilation output, which this method does not cover. For robust fingerprint management, a purpose-built antidetect browser generates consistent, hardware-backed profiles — see Method 9.
Method 8: Eliminating CDP and Automation Artifacts
Beyond JavaScript-level tells, Puppeteer leaves breadcrumbs in browser flags and environment variables. Removing these artifacts tightens your stealth significantly.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({
headless: false,
args: [
'--disable-blink-features=AutomationControlled',
'--disable-features=AutomationControlled',
'--disable-dev-shm-usage',
'--no-first-run',
'--no-default-browser-check',
'--disable-component-update',
// Remove automation flags from navigator
'--disable-backgrounding-occluded-windows',
],
ignoreDefaultArgs: ['--enable-automation'],
});
const page = await browser.newPage();
// Remove the webdriver property at the CDP level
await page.evaluateOnNewDocument(() => {
// Delete the webdriver property entirely
delete navigator.__proto__.webdriver;
// Remove the CDP runtime binding variable
// (its name varies across Chrome versions)
const cdcProps = Object.getOwnPropertyNames(window).filter((p) =>
p.match(/^cdc_/i)
);
cdcProps.forEach((prop) => {
delete window[prop];
});
// Spoof Chrome plugins array (headless Chrome has 0 plugins)
Object.defineProperty(navigator, 'plugins', {
get: () => {
const plugins = [
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
{ name: 'Native Client', filename: 'internal-nacl-plugin' },
];
plugins.length = 3;
return plugins;
},
});
// Spoof hardware concurrency and device memory
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
});
await page.goto('https://bot.sannysoft.com');
await page.screenshot({ path: 'cdp-cleaned.png' });
console.log('CDP artifacts removed — check cdp-cleaned.png');
await browser.close();
})();
The ignoreDefaultArgs: ['--enable-automation'] flag is critical — without it, Chrome starts with the automation-controlled flag that stealth plugins may not fully remove. Combine this with the --disable-blink-features=AutomationControlled argument for double coverage. If you want to learn how Selenium browser fingerprint detection works, many of these CDP artifacts overlap.
Method 9: Connect Puppeteer to an Antidetect Browser Profile
Every method above patches individual detection vectors, but anti-bot systems stack dozens of checks — and new ones appear constantly. The most reliable approach is to run Puppeteer against a browser profile that was built from the ground up to produce a consistent, human-like fingerprint across every dimension simultaneously. Antidetect browsers like Sendwin Browser do exactly this: each profile ships with its own canvas hash, WebGL renderer, audio fingerprint, timezone, locale, fonts, screen resolution, and hardware specs — all internally consistent.
Sendwin Browser exposes a local automation endpoint per profile. You launch the profile through the desktop app or the Automation API (available on Pro and Team plans), then connect Puppeteer to it over CDP — no stealth plugins needed, because the browser itself handles every fingerprint layer.
const puppeteer = require('puppeteer-core');
(async () => {
// Connect to a running Sendwin Browser profile via its local CDP endpoint.
// The profile's automation port is shown in the Sendwin Browser desktop app
// or returned by the Automation API when the profile is launched.
const profileCdpUrl = 'ws://127.0.0.1:PROFILE_PORT/devtools/browser';
const browser = await puppeteer.connect({
browserWSEndpoint: profileCdpUrl,
defaultViewport: null, // Use the profile's pre-configured viewport
});
const pages = await browser.pages();
const page = pages[0] || (await browser.newPage());
// No stealth plugin needed — the profile manages all fingerprints
await page.goto('https://bot.sannysoft.com');
await page.screenshot({ path: 'sendwin-profile-check.png' });
// Check detection results
const results = await page.evaluate(() => {
const rows = document.querySelectorAll('table tr');
const checks = {};
rows.forEach((row) => {
const cells = row.querySelectorAll('td');
if (cells.length >= 2) {
checks[cells[0].textContent.trim()] = cells[1].textContent.trim();
}
});
return checks;
});
console.log('Detection results:', results);
// Disconnect (don't close — the profile stays running)
browser.disconnect();
})();
This approach is the gold standard because you are not fighting Chrome’s internals with JavaScript patches — the browser itself presents a legitimate fingerprint to every check. Sendwin Browser’s profiles also persist cookies, localStorage, and browsing history between sessions, building long-term identity reputation that single-session stealth scripts cannot achieve.
Stealth Method Comparison
Not all techniques are equal. Here is how the nine methods stack up across the detection vectors that matter most:
| Method | navigator.webdriver | Headless Signals | CDP Artifacts | Canvas/WebGL | Behavioral | IP Reputation | Setup Effort |
|---|---|---|---|---|---|---|---|
| 1. Stealth Plugin | ✅ Fixed | ✅ Fixed | ⚠️ Partial | ⚠️ Basic | ❌ None | ❌ None | Low |
| 2. Custom User-Agent | ❌ No | ⚠️ Partial | ❌ No | ❌ No | ❌ None | ❌ None | Low |
| 3. Viewport Randomization | ❌ No | ⚠️ Partial | ❌ No | ❌ No | ❌ None | ❌ None | Low |
| 4. Mouse Simulation | ❌ No | ❌ No | ❌ No | ❌ No | ✅ Strong | ❌ None | Medium |
| 5. Proxy Rotation | ❌ No | ❌ No | ❌ No | ❌ No | ❌ None | ✅ Strong | Medium |
| 6. Timezone/Locale | ❌ No | ❌ No | ❌ No | ❌ No | ⚠️ Partial | ⚠️ Partial | Low |
| 7. Canvas/WebGL Spoofing | ❌ No | ❌ No | ❌ No | ✅ Strong | ❌ None | ❌ None | Medium |
| 8. CDP Cleanup | ✅ Fixed | ✅ Fixed | ✅ Fixed | ❌ No | ❌ None | ❌ None | Low |
| 9. Antidetect Browser | ✅ Fixed | ✅ Fixed | ✅ Fixed | ✅ Strong | ⚠️ Partial | ⚠️ Partial | Low |
The takeaway: methods 1-8 each solve one or two detection vectors well. An antidetect browser (method 9) covers the fingerprint stack comprehensively, but you still benefit from layering proxy rotation (method 5) and behavioral simulation (method 4) on top for the toughest targets.
Putting It All Together: Full Stealth Script
Here is a production-ready script that combines the stealth plugin, User-Agent consistency, viewport randomization, mouse simulation, proxy rotation, and CDP cleanup into a single reusable launcher.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
// --- Configuration ---
const USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
];
const VIEWPORTS = [
{ width: 1920, height: 1080, deviceScaleFactor: 1 },
{ width: 1366, height: 768, deviceScaleFactor: 1 },
{ width: 1536, height: 864, deviceScaleFactor: 1.25 },
];
function pick(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
async function humanDelay(min = 500, max = 2000) {
await new Promise((r) => setTimeout(r, min + Math.random() * (max - min)));
}
async function humanMove(page, targetX, targetY, steps = 20) {
const start = { x: Math.random() * 400, y: Math.random() * 400 };
const cp1x = start.x + (targetX - start.x) * (0.2 + Math.random() * 0.3);
const cp1y = start.y + (Math.random() - 0.5) * 150;
const cp2x = start.x + (targetX - start.x) * (0.6 + Math.random() * 0.3);
const cp2y = targetY + (Math.random() - 0.5) * 100;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const inv = 1 - t;
const x = inv ** 3 * start.x + 3 * inv ** 2 * t * cp1x + 3 * inv * t ** 2 * cp2x + t ** 3 * targetX;
const y = inv ** 3 * start.y + 3 * inv ** 2 * t * cp1y + 3 * inv * t ** 2 * cp2y + t ** 3 * targetY;
await page.mouse.move(x, y);
await new Promise((r) => setTimeout(r, 5 + Math.random() * 15));
}
}
// --- Main Launcher ---
async function launchStealthBrowser(proxyUrl = null) {
const args = [
'--disable-blink-features=AutomationControlled',
'--disable-features=AutomationControlled',
'--disable-infobars',
'--no-first-run',
'--no-default-browser-check',
'--disable-dev-shm-usage',
];
if (proxyUrl) {
args.push(`--proxy-server=${proxyUrl}`);
}
const vp = pick(VIEWPORTS);
args.push(`--window-size=${vp.width},${vp.height}`);
const browser = await puppeteer.launch({
headless: false,
args,
ignoreDefaultArgs: ['--enable-automation'],
});
const page = await browser.newPage();
const ua = pick(USER_AGENTS);
await page.setUserAgent(ua);
await page.setViewport(vp);
// Apply all stealth overrides
await page.evaluateOnNewDocument((viewport) => {
// Remove webdriver
delete navigator.__proto__.webdriver;
// Remove CDP bindings
Object.getOwnPropertyNames(window)
.filter((p) => /^cdc_/i.test(p))
.forEach((p) => delete window[p]);
// Spoof screen
Object.defineProperty(screen, 'width', { get: () => viewport.width });
Object.defineProperty(screen, 'height', { get: () => viewport.height });
// Spoof plugins
Object.defineProperty(navigator, 'plugins', {
get: () => {
const p = [
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
{ name: 'Native Client', filename: 'internal-nacl-plugin' },
];
p.length = 3;
return p;
},
});
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
}, vp);
if (proxyUrl) {
const url = new URL(proxyUrl);
await page.authenticate({
username: url.username,
password: url.password,
});
}
return { browser, page, humanMove, humanDelay };
}
// --- Usage Example ---
(async () => {
const { browser, page, humanMove, humanDelay } = await launchStealthBrowser(
// 'http://user:[email protected]:8080' // Uncomment to use a proxy
);
await page.goto('https://bot.sannysoft.com', { waitUntil: 'networkidle2' });
await humanDelay(1000, 3000);
// Simulate natural browsing behavior
await humanMove(page, 400, 300);
await page.mouse.wheel({ deltaY: 300 });
await humanDelay(500, 1500);
await page.screenshot({ path: 'full-stealth-check.png', fullPage: true });
console.log('Full stealth check complete — see full-stealth-check.png');
await browser.close();
})();
Common Pitfalls and How to Fix Them
Running in Headless Mode
Even Chrome’s --headless=new mode leaks signals that anti-bot scripts detect. If you absolutely must run headless (e.g., in CI/CD), use Xvfb on Linux to run a “headed” Chrome without a physical display:
// Launch with xvfb-run on Linux for headless-but-headed Chrome
// Shell: xvfb-run --auto-servernum node script.js
const browser = await puppeteer.launch({
headless: false, // Real headed mode inside virtual display
args: ['--no-sandbox'],
});
Forgetting to Handle WebSocket Connections
When connecting to remote browsers (method 9), always use browser.disconnect() instead of browser.close() to avoid killing the profile session.
Using Outdated Stealth Plugin Versions
Anti-bot systems update their detection logic frequently. Pin puppeteer-extra-plugin-stealth to the latest version in your package.json and update at least monthly. An outdated stealth plugin can be worse than none — it may override a property in a way that a newer detector specifically looks for.
Inconsistent Fingerprint Dimensions
Setting a MacOS User-Agent but leaving the platform as “Win32,” or spoofing a mobile viewport with desktop hardware concurrency, creates detectable contradictions. Always audit your full fingerprint at browserleaks.com before running scraping jobs.
🏆 Send.win Verdict
JavaScript-level stealth patches (methods 1-8) buy time, but anti-bot platforms keep adding new detection vectors — maintaining a patchwork of evaluateOnNewDocument overrides is a constant arms race. Sendwin Browser eliminates this entirely: each profile generates a hardware-consistent fingerprint across canvas, WebGL, audio, fonts, screen resolution, and navigator properties, so you can connect Puppeteer and automate without writing a single stealth hack. The Automation API (available on Pro at $9.99/mo or $6.99/mo annual, and Team at $29.99/mo or $20.99/mo annual) exposes a local CDP endpoint per profile, meaning your Puppeteer scripts stay clean — just puppeteer.connect() and go. With 150 profiles and 5 GB on Pro, or 500 profiles, 20 GB, and 16 team seats on Team, you can run large-scale automation with unique, persistent identities that build reputation over time instead of resetting every session.
Try Send.win free today — 30-day trial, no credit card, full Automation API access from day one.
Frequently Asked Questions
Can Puppeteer be detected even with the stealth plugin?
Yes. The stealth plugin patches common detection vectors like navigator.webdriver and headless Chrome signals, but it does not cover canvas/WebGL fingerprinting, behavioral analysis, or IP reputation checks. Advanced anti-bot platforms like Cloudflare Turnstile and DataDome use layered detection that catches stealth-plugin-only setups. You need to combine multiple techniques — or use an antidetect browser — for reliable evasion.
Is headless Chrome more detectable than headed Chrome?
Significantly. Headless Chrome (even the newer --headless=new flag) produces different rendering output for canvas and WebGL, lacks browser plugins, and exposes subtle JavaScript differences that anti-bot scripts fingerprint. Running in headed mode (with a virtual display like Xvfb if no monitor is available) immediately eliminates an entire class of detection signals.
What is the best proxy type for Puppeteer stealth?
Residential proxies with sticky sessions are the gold standard. They use real ISP-assigned IP addresses that anti-bot systems treat as legitimate user traffic. Mobile proxies are even more trusted but slower and more expensive. Avoid datacenter proxies for anything protected by Cloudflare, Akamai, or PerimeterX — they are flagged almost instantly. Budget roughly $5-15 per GB for quality residential bandwidth.
How do I test if my Puppeteer setup is detectable?
Visit these test pages with your automated browser: bot.sannysoft.com checks webdriver flags and browser properties; browserleaks.com tests canvas, WebGL, audio, and font fingerprints; pixelscan.net runs a comprehensive fingerprint consistency check; and creepjs.com is the most aggressive detector, checking for lie detection and tampered APIs. If you pass all four, your setup handles most commercial anti-bot systems.
Does puppeteer-extra-plugin-stealth work with Puppeteer v22+?
Yes, the stealth plugin is actively maintained and compatible with Puppeteer v22 and later. However, always verify the latest release notes on the puppeteer-extra GitHub repository — Chrome updates occasionally break specific evasion modules, and patches may lag by a few days. Pin your Puppeteer and stealth plugin versions together to avoid compatibility gaps.
Can websites detect Puppeteer through network traffic patterns?
Yes. Puppeteer scripts often make requests in rapid, perfectly-timed sequences with no idle gaps — a pattern no human browsing session produces. Anti-bot platforms analyze request timing, the ratio of asset loading to navigation events, and whether the browser loads resources like fonts and images that a real user’s browser would cache. Adding random delays between actions and loading pages fully (including assets) reduces this risk.
What is the difference between puppeteer-extra-plugin-stealth and connecting to an antidetect browser?
The stealth plugin patches Chrome’s JavaScript APIs to hide automation signals — it modifies what the browser reports but does not change the underlying rendering engine. An antidetect browser like Sendwin Browser generates a completely separate browser profile with its own canvas renderer output, WebGL driver response, font list, and hardware identifiers. The stealth plugin fights detection at the surface; an antidetect browser eliminates it at the source. For high-value targets, the antidetect approach is far more reliable.
How many Puppeteer instances can I run simultaneously?
Each headed Chrome instance uses roughly 200-400 MB of RAM plus CPU for rendering. On a typical 16 GB machine, you can comfortably run 8-12 concurrent instances. If you need more scale, Sendwin Browser’s cloud browser sessions let you offload profile execution to the cloud without consuming local resources — useful when running 50+ profiles simultaneously for large-scale automation tasks.
Automate Puppeteer Avoid Bot Detection With Send.win
Send.win pairs isolated, fingerprint-managed browser profiles with a full Automation API, so your scripts run in profiles that look and behave like real, separate users:
- Selenium, Puppeteer & Playwright support – drive any profile programmatically (Team plan)
- Isolated profiles – each with its own fingerprint, cookies, and storage
- Built-in residential proxies – with automatic timezone, locale, and WebRTC matching
- Desktop app for Windows, macOS & Linux – plus cloud sessions when you don’t want a local install
Try the instant cloud browser demo — no install, straight from your browser. Then compare plans: a 30-day free trial with no credit card, and paid plans from $6.99/month billed annually.