Cloudflare often acts like a reverse proxy — not for visitors, but for the sites themselves. It’s an advanced CDN service that can simultaneously balance load, protect sites from DDoS attacks, detect bots, act as a web application firewall, and secure DNS access.
In this piece, we’ll discuss one of the most effective tools for working with dynamic websites — Puppeteer. For a deeper dive, see our Puppeteer web scraping guide. In short, Puppeteer is a web driver for a headless Chrome browser that uses the Chrome DevTools Protocol. It’s implemented in JavaScript (although there are unofficial ports for other languages, Puppeteer can be controlled via API, so its language lock-in is mostly nominal). Recent releases added support for Firefox and the BiDi protocol.
So, to create a reliable bypass for Cloudflare, you first need to install and configure Puppeteer.
Setting Up Puppeteer for Web Scraping
Puppeteer runs in a Node.js environment. Don’t confuse it with Pyppeteer, which is a Python fork.
Node.js is a JavaScript runtime built on Google Chrome’s V8 engine. In other words, it lets you execute JavaScript code outside the browser, on the server side. Node.js comes with a mature ecosystem of packages and modules, APIs for almost any use case, and an open-source foundation. Architecturally, it’s similar to the Twisted framework in Python or the EventMachine event-processing library for Ruby.
Suppose Node.js isn’t yet installed on your PC or server. In that case, you must download the installer from the official website or install Node from your OS package repository (common for Linux distributions).
At the time of writing:
- The latest LTS version is Node.js 22.19.
- The current stable release is 24.8.
The developers strongly recommend using Docker as the runtime environment for Windows systems. Alternatives include Chocolatey and Volta. On other operating systems, there are more options, such as Brew, n, Devbox, fnm, or nvm.
You can use npm, Yarn, or pnpm for package management. The choice usually depends more on team preference than on technical differences.
Important: When installing Node.js, don’t forget to add Node and your chosen package manager to your system’s environment variables (PATH).
Installing Node.js and Puppeteer with Docker
If Docker is installed on your system, the installation of Node.js follows a simple sequence:
- Pull the Node.js image (for example, version 24; for the LTS release, version 22 is used).
- Run a container and connect to it.
- Check the current Node.js version, which should display something like v24.8.0.
- Check the NPM version, which will return a value such as 11.6.0.
Once Node.js is confirmed to be working, you can move on to installing the Puppeteer web scraping library:
- Install Puppeteer together with a compatible headless Chrome browser and all dependencies.
- Alternatively, install only Puppeteer without downloading Chrome.
- Run the corresponding installation command if you use Yarn as a package manager.
After this setup, you can create JavaScript scraping scripts with Cloudflare bypass.
Puppeteer Techniques for Handling Cloudflare

Cloudflare operates on a large distributed network of servers (points of presence in more than 120 countries). Traffic is analyzed across many signals and can be blocked before it even reaches the target site if suspicious patterns are detected.
Because of this, you must be especially careful to ensure that your scraper's behavior and digital fingerprint do not differ from those of real users. How can you achieve that? Let’s review the main approaches to bypassing Cloudflare protection.
As a caveat: there is no universal solution, and Cloudflare’s defenses are constantly evolving and becoming more sophisticated. For that reason, you should rely not on any single technique but on a combination of methods.
So, here are the aspects to pay special attention to when building a Cloudflare bypass.
Hiding Headless-Browser Indicators
Any browser carries digital fingerprints and specific marks in HTTP headers. To reduce the chance of detecting Headless Chrome controlled by Puppeteer, consider the following measures:
- Disable the navigator.webdriver attribute (it explicitly indicates automation). Set navigator.webdriver to false for a Cloudflare bypass.
- Use a natural User-Agent and other headers (remove any explicit “headless” marker). Replace the headless UA with the same string a stable, regular Chrome would use.
- Update other browser settings to look normal: language, screen resolution, available system fonts, WebGL / GPU profile, and the list of installed plugins (a complete absence of extensions can also seem suspicious). Ensure AudioContext is not in an “offline” state. Configure mediaDevices to report a realistic device set. Be aware of WebRTC addresses because that protocol can leak a different IP address than your proxy.
- Install puppeteer-extra and the stealth plugin (puppeteer-extra-plugin-stealth) to reduce common fingerprints.
- If problems persist, disable headless mode and run the browser in visible/headful mode (i.e., headless: false).
- Instead of the bundled headless Chrome, consider automating a real browser binary with its authentic digital profile (use the real Chrome executable path).
Mandatory Check for Cloudflare IUAM Redirects
IUAM (short for "I'm Under Attack Mode") is a special protection mechanism used by sites that are currently under bot attack, such as during a DDoS event. Cloudflare’s bot protection can usually only be bypassed by a real human. For instance, the user may have to solve a CAPTCHA or tick an “I am human” checkbox (during which pointer movements and other behavioral signals are analyzed).
To avoid more serious countermeasures, your Puppeteer scraping script should monitor for 503 errors and watch for JavaScript challenges (challenge-platform or Turnstile, Cloudflare’s newer challenge/CAPTCHA system).
When first accessing a site, it’s sensible to wait for a short pause of around 7–8 seconds and check for special cookies (they often include strings like cf_clearance or __cf_bm) that indicate the “humanity” check was passed. These cookies remain valid for the session, so once you obtain a trusted status, try to preserve it — keep the same proxy IP and persist cookies for subsequent requests.
Also note that some Cloudflare settings are stored in the __cflb cookie; it’s useful to retain this cookie as well, since it can remain valid for up to 23 hours.
Residential Proxies
Perfect proxies for accessing valuable data from around the world.
Solving CAPTCHAs (Cloudflare Turnstile, reCAPTCHA)
Because Cloudflare may show CAPTCHAs even to legitimate users despite their trusted digital profiles and behavior, you need to plan for cases where a CAPTCHA must be solved.
Options to consider:
- Disable headless mode or pause the scraper so an operator can solve the CAPTCHA manually. At a large scale, this becomes a bottleneck — one person can’t keep up with many simultaneous CAPTCHAs.
- Integrate a third-party CAPTCHA-solving service into your Puppeteer script to achieve multithreading and support for multiple CAPTCHA formats. Common services include 2captcha, CapMonster, AntiCaptcha, and similar providers. For faster integration, there is an available plugin called puppeteer-extra-plugin-recaptcha (primarily aimed at Google reCAPTCHA).
- Remember that CAPTCHAs can be hard and may not be solved on the first try. Implement retry logic and a clear policy for skipping or moving on to the next page when repeated attempts fail.
- To reduce CAPTCHA triggers in some cases, pretend to be a different browser by rotating or changing the user-agent string. There are ready-made npm modules for randomizing user agents, such as random-useragent.
How to Detect When Cloudflare Displays a CAPTCHA
You can analyze the HTML code for textual markers such as “Checking your browser…” or “Please enable JavaScript and Cookies to continue.” These messages usually appear on an intermediate redirect page.
However, the most reliable method is to inspect the page structure for specific elements:
- A div block with the class cf-turnstile or containing a data-sitekey attribute.
- A div block with the class g-recaptcha or a script referencing https://www.google.com/recaptcha/api.js.
- An iframe whose source URL includes /recaptcha/, or an iframe with a title that contains “reCAPTCHA” or “Turnstile.”
Proxy Rotation
Since Cloudflare blocks are tied to specific user IP addresses, you can take the opposite approach and:
- Change IP addresses quickly for each new request.
- Rotate user-agent strings and browser fingerprints. To automate this process and generate more natural digital profiles, anti-detect browsers can be used. These tools also allow each browser instance to connect through a separate proxy.
Dynamic proxy rotation may fail if Cloudflare’s IUAM protection is active. In such cases, it’s better to obtain trusted cookies and maintain the session for as long as possible. Otherwise, you’ll end up solving a CAPTCHA for every new connection.
To avoid manual proxy switching, it’s best to connect through a reliable proxy service such as Froxy. Rotation logic can be configured individually for each proxy port. With Cloudflare bypass scrapers, proxies are connected only once (via the BACKCONNECT scheme). The exit IPs can then be selected not only by location but also by ISP (Internet Service Provider).
The most trusted proxies for working with Cloudflare are mobile and residential ones.
Ethics and Limitations
No method for bypassing Cloudflare guarantees 100% success: bot and scraper signatures are constantly being updated. The more automated requests you send to sites protected by Cloudflare, the higher the chance that a signature will be created specifically for your scraper.
Massive attempts to bypass target-site protections can violate the site’s internal rules and/or the laws of the country where it operates. Always read the legal and user agreements carefully — otherwise, you risk lawsuits and other penalties.
Some evasions (for example, overly aggressive WebGL spoofing) may break page rendering and site scripts. Make only the minimal changes necessary.
Stealth plugins and automated solutions become outdated over time — check and update them periodically. Develop and deploy your own indicators/attributes that Cloudflare’s defenses might flag, and adapt as those defenses evolve.
Full Puppeteer Example Script

Create a directory on disk, switch into it, and initialize the project:
mkdir cloudflare-scrapercd cloudflare-scrapernpm init -y
Don't forget to install all required Puppeteer libraries for scraping (we use the stealth plugin and filesystem utilities):
npm install puppeteer-extra puppeteer-extra-plugin-stealth puppeteer fs-extra
/**
* NOTES!
* Template: Puppeteer + Stealth + 2captcha
* Configure:
* - PROXY (rotating) or an array of proxies
* - PATH_TO_CHROME (if you want to use a real Chrome browser instead of headless)
* - API_KEY_2CAPTCHA
*
* Note: the example uses global fetch (Node v18+). For older Node use node-fetch/axios.
*/
// Import libraries
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const fs = require('fs-extra');
const path = require('path');
// Use stealth mode
puppeteer.use(StealthPlugin());
// ========== CONFIG ==========
const API_KEY_2CAPTCHA = 'REPLACE_WITH_YOUR_2CAPTCHA_KEY';
const PROFILE_DIR = path.resolve(__dirname, 'profiles'); // path for saving profile and cookies
const COOKIES_FILE = profileId => path.join(PROFILE_DIR, `${profileId}_cookies.json`);
const PATH_TO_CHROME = undefined; // path to a real browser '/usr/bin/google-chrome' or undefined to use the bundled headless browser
const HEADLESS = false; // we recommend false to start
// Proxy: if you have a rotator — just pass the proxy server (socks5/http)
const PROXY = 'http://username:password@proxy-host:port'; // or null
// ========== HELPERS ==========
// Async sleep and promises
async function sleep(ms){ return new Promise(r => setTimeout(r, ms)); }
// Save cookies
async function saveCookies(profileId, page){
await fs.ensureDir(PROFILE_DIR);
const cookies = await page.cookies();
await fs.writeJson(COOKIES_FILE(profileId), cookies, { spaces: 2 });
}
// Load existing cookies
async function loadCookies(profileId, page){
const f = COOKIES_FILE(profileId);
// If cookies file exists, read it
if (await fs.pathExists(f)) {
const cookies = await fs.readJson(f);
try {
await page.setCookie(...cookies);
return true;
} catch(e){
// If cookies can't be set, log the error to the console
console.warn(Error when installing cookies:', e.message);
}
}
return false;
}
// 2captcha: submit task and poll result
async function submit2CaptchaTask({ apiKey, sitekey, pageurl, method }) {
// method: 'userrecaptcha' | 'hcaptcha' | 'turnstile' etc.
// returns { requestId }
const params = new URLSearchParams({
key: apiKey,
method,
googlekey: sitekey, // for recaptcha userrecaptcha
pageurl
});
// for turnstile 2captcha requires method "method=turnstile" and "sitekey"
if (method === 'turnstile') {
params.delete('googlekey');
params.set('sitekey', sitekey);
}
params.set('json', 1);
const res = await fetch(`http://2captcha.com/in.php?${params.toString()}`);
const json = await res.json();
if (json.status !== 1) throw new Error('2captcha submit error: ' + JSON.stringify(json));
return { requestId: json.request };
}
// Set the number of CAPTCHA-solving attempts and the interval between requests (in milliseconds)
async function poll2CaptchaResult({ apiKey, requestId, maxAttempts = 10, interval = 5000 }) {
const params = new URLSearchParams({
key: apiKey,
action: 'get',
id: requestId,
json: 1
});
for (let i = 0; i < maxAttempts; i++){
await sleep(interval);
const res = await fetch(`http://2captcha.com/res.php?${params.toString()}`);
const json = await res.json();
if (json.status === 1) {
return json.request; // token
} else if (json.request === 'CAPCHA_NOT_READY') {
// keep waiting
console.log(`2captcha: not ready (${i+1}/${maxAttempts})`);
continue;
} else {
// log error to console
throw new Error('2captcha error: ' + JSON.stringify(json));
}
}
throw new Error('2captcha: timeout waiting for solution');
}
// CAPTCHA detection in HTML/DOM
async function detectCaptchaOnPage(page){
// Wait for content to load
const html = await page.content();
// quick HTML check, look for signs of recaptcha, hcaptcha, and turnstile
const quick = /challenges\.cloudflare\.com\/turnstile|cdn-cgi\/challenge-platform|g-recaptcha|hcaptcha|www\.google\.com\/recaptcha|Checking your browser|One more step/i.test(html);
// look for selectors and other indicators
const hasRecaptcha = !!(await page.$('.g-recaptcha, [data-sitekey][data-widget-id*="recaptcha"], iframe[src*="recaptcha"]'));
const hasHcaptcha = !!(await page.$('.h-captcha, iframe[src*="hcaptcha.com"]'));
const hasTurnstile = !!(await page.$('[data-cf-turnstile], .cf-turnstile, iframe[src*="turnstile"]'));
// try to find an explicit sitekey in HTML
const sitekeyMatch = html.match(/data-sitekey=["']([^"']+)["']/i);
const iframeUrls = (await page.frames()).map(f => f.url()).join(' ');
const iframeSitekeyMatch = iframeUrls.match(/sitekey=([^&]+)/i);
// check cookies
const cookies = await page.cookies();
const cookieNames = cookies.map(c => c.name).join(',');
const hasCfClearance = /cf_clearance/i.test(cookieNames);
return {
quick,
selectors: { hasRecaptcha, hasHcaptcha, hasTurnstile },
sitekey: (sitekeyMatch && sitekeyMatch[1]) || (iframeSitekeyMatch && decodeURIComponent(iframeSitekeyMatch[1])) || null,
cookieNames,
hasCfClearance,
iframeUrls
};
}
// return the solved CAPTCHA into the DOM and submit the form
async function injectCaptchaToken(page, token, type){
// type: 'recaptcha' | 'hcaptcha' | 'turnstile'
if (type === 'recaptcha'){
// classic: g-recaptcha-response textarea
await page.evaluate((t) => {
let el = document.querySelector('textarea[name="g-recaptcha-response"]');
if (!el) {
el = document.createElement('textarea');
el.name = 'g-recaptcha-response';
el.style.display = 'none';
document.body.appendChild(el);
}
el.value = t;
// some sites expect an explicit callback trigger
if (window.grecaptcha && grecaptcha && grecaptcha.getResponse) {
}
}, token);
// attempt to inject the solution
await page.evaluate(() => {
// triggers that some forms listen for
document.dispatchEvent(new Event('recaptcha-token-injected'));
});
} else if (type === 'turnstile'){
await page.evaluate((t) => {
// typical Cloudflare Turnstile input name: cf-turnstile-response
let el = document.querySelector('textarea[name="cf-turnstile-response"], input[name="cf-turnstile-response"]');
if (!el) {
el = document.createElement('textarea');
el.name = 'cf-turnstile-response';
el.style.display = 'none';
document.body.appendChild(el);
}
el.value = t;
document.dispatchEvent(new Event('turnstile-token-injected'));
}, token);
} else if (type === 'hcaptcha'){
// hcaptcha solution injection
await page.evaluate((t) => {
let el = document.querySelector('textarea[name="h-captcha-response"], input[name="h-captcha-response"]');
if (!el) {
el = document.createElement('textarea');
el.name = 'h-captcha-response';
el.style.display = 'none';
document.body.appendChild(el);
}
el.value = t;
document.dispatchEvent(new Event('hcaptcha-token-injected'));
}, token);
}
// try to auto-submit the form if one exists
try {
await page.evaluate(() => {
const forms = Array.from(document.forms);
if (forms.length === 1) forms[0].submit();
else {
// if a submit button exists
const btn = document.querySelector('button[type="submit"], input[type="submit"]');
if (btn) btn.click();
}
});
} catch(e){
console.warn('Error when auto-submitting the form:', e.message);
}
}
// Main scraper
async function runScrape({ url, profileId = 'default', proxy = PROXY }) {
// Additional arguments to hide headless browser traces
const launchArgs = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-blink-features=AutomationControlled',
'--lang=en-US,en'
];
if (proxy) launchArgs.push(`--proxy-server=${proxy}`);
const browser = await puppeteer.launch({
headless: HEADLESS,
executablePath: PATH_TO_CHROME || undefined,
args: launchArgs,
// It's a good idea to set realistic browser window parameters
defaultViewport: { width: 1366, height: 768 }
});
const page = await browser.newPage();
// HTTP headers: user agent and extra headers
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36');
await page.setExtraHTTPHeaders({ 'Accept-Language': en-US,en;q=0.9,en-US;q=0.8,en;q=0.7' });
// Important: early overrides before page load
await page.evaluateOnNewDocument(() => {
// navigator.webdriver
Object.defineProperty(navigator, 'webdriver', { get: () => false });
// languages
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en', 'ru-RU', 'ru'] });
// plugins - simple emulation
Object.defineProperty(navigator, 'plugins', {
get: () => [{ name: 'Chrome PDF Plugin' }, { name: 'Native Client' }]
});
// chrome runtime
window.chrome = window.chrome || { runtime: {} };
// WebGL patch (injecting a primitive emulation)
try {
const getParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(parameter) {
if (parameter === 37445) return 'Intel Inc.';
if (parameter === 37446) return 'Intel(R) UHD Graphics 620';
return getParameter.call(this, parameter);
};
} catch(e){}
try {
const origQuery = navigator.permissions.query;
navigator.permissions.query = (params) => (
params.name === 'notifications' ? Promise.resolve({ state: Notification.permission }) : origQuery(params)
);
} catch(e){}
});
// Try to load cookies from profile
await loadCookies(profileId, page);
// Create a CDP session to override timezone, locale, and geolocation if needed
const client = await page.target().createCDPSession();
try {
await client.send('Emulation.setTimezoneOverride', { timezoneId: 'America/New_York' });
await client.send('Emulation.setLocaleOverride', { locale: 'en_US' });
// geolocation example (uncomment if needed)
// await client.send('Emulation.setGeolocationOverride', { latitude: 55.75, longitude: 37.616, accuracy: 20 });
} catch(e){ console.warn('CDP emulation not available:', e.message); }
// Navigation + Cloudflare IUAM and CAPTCHA detection
console.log('Opening the address:', url);
const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }).catch(e => null);
// give the JS challenge some time to execute
await sleep(8000);
// additionally, wait for navigation if redirects occur
try { await page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 5000 }).catch(()=>{}); } catch(e){}
// detection
const detection = await detectCaptchaOnPage(page);
console.log('Detection:', detection.selectors, 'quickHtml:', detection.quick);
console.log('Cookies:', detection.cookieNames);
if (detection.hasCfClearance) {
console.log('Cloudflare clearance cookie present — most likely the check was passed.');
}
// If a CAPTCHA is detected — solve it via 2captcha
if (detection.selectors.hasRecaptcha || detection.selectors.hasHcaptcha || detection.selectors.hasTurnstile) {
const sitekey = detection.sitekey;
console.log('sitekey detected:', sitekey);
let captchaType = null;
if (detection.selectors.hasRecaptcha) captchaType = 'recaptcha';
else if (detection.selectors.hasHcaptcha) captchaType = 'hcaptcha';
else if (detection.selectors.hasTurnstile) captchaType = 'turnstile';
if (!sitekey) {
// attempt to extract sitekey from iframe URLs
const iframeUrls = detection.iframeUrls || '';
const sk = iframeUrls.match(/sitekey=([^&]+)/i);
if (sk) {
detection.sitekey = decodeURIComponent(sk[1]);
}
}
if (!detection.sitekey) {
console.warn('The sitekey could not be found automatically. Try manually specifying a sitekey or adding a detector.');
} else {
try {
console.log('Sending the task to 2captcha (type=${captchaType})...');
const methodMap = { recaptcha: 'userrecaptcha', hcaptcha: 'hcaptcha', turnstile: 'turnstile' };
const submit = await submit2CaptchaTask({
apiKey: API_KEY_2CAPTCHA,
sitekey: detection.sitekey,
pageurl: url,
method: methodMap[captchaType]
});
console.log('2captcha requestId=', submit.requestId || submit.request);
const requestId = submit.requestId || submit.request;
const token = await poll2CaptchaResult({ apiKey: API_KEY_2CAPTCHA, requestId });
console.log('2captcha token received, inserted into the page:', token.substring(0, 20) + '...');
// inject the token and attempt auto-submit
await injectCaptchaToken(page, token, captchaType);
// wait for redirect / page update
await sleep(3000);
await page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 15000 }).catch(()=>{});
console.log('We check the result after inserting the token...');
const afterDetection = await detectCaptchaOnPage(page);
console.log('After:', afterDetection.selectors, 'cookies:', afterDetection.cookieNames);
} catch (e) {
console.error('Error solving captcha:', e.message);
}
}
} else {
console.log('Captcha not found on the page (by selectors).');
}
// If everything is OK — save cookies for subsequent sessions
await saveCookies(profileId, page);
console.log('Cookies saved.');
// next — custom page parsing
const content = await page.content();
// TODO: parse required data
console.log('Length HTML:', content.length);
// Finish
await browser.close();
}
// ========== Run ==========
(async () => {
try {
const targetUrl = process.argv[2] || 'https://target-site.example/';
await runScrape({ url: targetUrl, profileId: 'default' });
console.log('Done.');
} catch (e) {
console.error('Fatal error:', e);
}
})();
Save the script to cloudflare_puppeteer_scraper.js and run it from the console:
node cloudflare_puppeteer_scraper.js 'https://target-site.example/'
Conclusion

We’ll stress again: there is no universal solution to bypass Cloudflare, and there never will be. Both protection mechanisms and the tools used to evade them change constantly. This is an ongoing arms race.
It’s not enough to just tweak the basic signals Cloudflare looks at (user agents, HTTP headers, etc.). You need a comprehensive approach: use proxies, emulate real user behavior, maintain plausible fingerprints, randomize delays between requests, and so on. Encountering a CAPTCHA doesn’t mean failure —on the contrary, passing such a check manually can significantly increase your scraper’s trust profile.
For renting high-quality rotating proxies, welcome to Froxy. We provide more than 10 million residential and mobile proxies worldwide. You can also try a ready-made scraper (Froxy Scraper) —with it, you may not need to solve any CAPTCHAs at all.

