DEV Community

Cover image for What I'm Using in NodeJS to Bypass Cloudflare Challenge and Turnstile
Mert Bekci
Mert Bekci

Posted on

What I'm Using in NodeJS to Bypass Cloudflare Challenge and Turnstile

Over the years, I’ve found many ways to bypass Cloudflare.

And over the years, Cloudflare has learned to stop almost every one of them.

Last weekend, I hit a wall trying to scrape a Cloudflare-protected site. Most of my old methods failed; some silently, some painfully.

Goodbye old friends. 😿

So I wiped the board clean and rebuilt my entire stack from scratch.

Here’s what’s reliably bypassing Cloudflare Challenge (and Turnstile CAPTCHA) in NodeJS right now as of July 2025:

Using Selenium Webdriver: Manual Turnstile Click + Rotating Proxies

There’s no undetected-chromedriver for Node.js. No plug-and-play stealth plugin to make everything magically work.

But if you’re using vanilla selenium-webdriver and Chrome, you can still bypass most Cloudflare challenges by clicking the thing yourself.

That’s what this method does:

It finds the invisible Turnstile checkbox, calculates its screen position, and simulates a real mouse click at the right spot.

Here’s how it works:

1. Locate the Hidden Turnstile Anchor

Cloudflare hides Turnstile behind a closed shadow DOM, which means traditional selectors won’t help.

But every challenge page still includes a hidden input like this:

<input type="hidden" name="cf-turnstile-response" />
Enter fullscreen mode Exit fullscreen mode

We can’t click it. But we can find its parent, which visually contains the checkbox.

const cfInput = await driver.findElement(By.css('[name="cf-turnstile-response"]'));
const parent = await driver.executeScript('return arguments[0].parentElement;', cfInput);
Enter fullscreen mode Exit fullscreen mode

2. Get screen coordinates

Once we have the parent element, we ask the browser for its bounding box:

const rect = await driver.executeScript(`
  const r = arguments[0].getBoundingClientRect();
  return {x: r.x, y: r.y, width: r.width, height: r.height};
`, parent);
Enter fullscreen mode Exit fullscreen mode

Now we have a rough hitbox for the Turnstile container.

3. Click like a human

Instead of calling .click() (which Turnstile ignores), we simulate a real mouse movement and click inside the box:

const clickX = rect.x + 25;
const clickY = rect.y + rect.height / 2;

await driver.actions({ bridge: true })
  .move({ x: Math.round(clickX), y: Math.round(clickY) })
  .click()
  .perform();
Enter fullscreen mode Exit fullscreen mode

This works well in regular Cloudflare challenges that might or might not trigger a turnstile. BUT if the turnstile challenge is forced, the success rate will drop.

4. Let it breathe and take a screenshot

Turnstile takes a few seconds to validate. We wait, then screenshot the result.

await driver.sleep(15000);
const image = await driver.takeScreenshot();
fs.writeFileSync('screenshot.png', image, 'base64');
Enter fullscreen mode Exit fullscreen mode

Final code

const { Builder, By } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome.js');
const fs = require('fs');

const url = 'https://scrapingtest.com/cloudflare-challenge';

async function clickTurnstile(driver) {
    try {
        const cfInput = await driver.findElement(By.css('[name="cf-turnstile-response"]'));
        const parent = await driver.executeScript('return arguments[0].parentElement;', cfInput);
        const rect = await driver.executeScript(`const r = arguments[0].getBoundingClientRect(); return {x: r.x, y: r.y, width: r.width, height: r.height};`, parent);
        const clickX = rect.x + 25;
        const clickY = rect.y + (rect.height / 2);
        await driver.actions({bridge: true}).move({x: Math.round(clickX), y: Math.round(clickY)}).click().perform();
        console.log('Clicked Turnstile checkbox.');
    } catch (e) {
        console.log('Turnstile was not triggered.');
    }
}

async function scrapeWithUndetectedBrowser() {
    const options = new chrome.Options();
    options.addArguments('--no-sandbox', '--disable-blink-features=AutomationControlled');
    const driver = await new Builder().forBrowser('chrome').setChromeOptions(options).build();

    try {
        await driver.get(url);
        await driver.sleep(5000);
        await clickTurnstile(driver);
        await driver.sleep(15000);
        const image = await driver.takeScreenshot();
        fs.writeFileSync('screenshot.png', image, 'base64');
        console.log('Status Code: 200 (browser)');
        console.log('Screenshot saved as screenshot.png');
    } finally {
        await driver.quit();
    }
}

scrapeWithUndetectedBrowser().catch(console.error);
Enter fullscreen mode Exit fullscreen mode

And here's what we have in the screenshot:

🧠 Pro Tip: This method works way better when you combine it with rotating proxies especially residential or mobile ones. Even cheap ones will improve your success rate dramatically.

Using Puppeteer: Rebrowser-Puppeteer + Stealth Plugin

If the Selenium setup was a clever trick, this one is a relentless weapon in your arsenal.

This method uses Rebrowser-Puppeteer, which is essentially Puppeteer bundled with a real, up-to-date Chrome that mimics human fingerprints much better than the default Chromium.

On top of that, we load puppeteer-extra and plug in the stealth plugin to patch over the usual automation tells (things like WebGL leaks, iframe inconsistencies, and weird canvas behavior).

I also disable the user-agent override evasion; Cloudflare already checks so many other things that messing with the UA string often does more harm than good.

Here's the setup:

const vanillaPuppeteer = require('rebrowser-puppeteer');
const { addExtra } = require('puppeteer-extra');
const puppeteer = addExtra(vanillaPuppeteer);
const createPuppeteerStealth = require('puppeteer-extra-plugin-stealth');
const puppeteerStealth = createPuppeteerStealth();
puppeteerStealth.enabledEvasions.delete('user-agent-override');
puppeteer.use(puppeteerStealth);
Enter fullscreen mode Exit fullscreen mode

We launch Chrome in non-headless mode, strip out --enable-automation, and disable some browser features that can reveal automation.

Then we hit the target page and start looping.

The idea is simple: every few seconds, we check if we’ve made it past the challenge, and keep clicking with the same trick as before, but done with Puppeteer’s mouse and much more relentlessly.

The rest is just patience. Wait, retry, click if needed. And eventually, it lets you through.

Once you've bypassed, we take a screenshot and close the browser.

Here’s the full code:

const vanillaPuppeteer = require('rebrowser-puppeteer');
const { addExtra } = require('puppeteer-extra');
const puppeteer = addExtra(vanillaPuppeteer);
const createPuppeteerStealth = require('puppeteer-extra-plugin-stealth');
const puppeteerStealth = createPuppeteerStealth();
puppeteerStealth.enabledEvasions.delete('user-agent-override');
puppeteer.use(puppeteerStealth);

async function scrape() {
    let browser = null;
    try {
        browser = await puppeteer.launch({
            ignoreDefaultArgs: [
                '--enable-automation',
            ],
            headless: false,
            args: [
                '--disable-features=Translate,AcceptCHFrame,MediaRouter,OptimizationHints,ProcessPerSiteUpToMainFrameThreshold,IsolateSandboxedIframes,AutomationControlled'
            ],
            defaultViewport: null,
        });

        const [page] = await browser.pages();
        const url = 'https://scrapingtest.com/cloudflare-turnstile';
        let response = await page.goto(url, { waitUntil: 'domcontentloaded' });
        let status = response.status();

        // Wait for page to load and try to click Turnstile
        await new Promise(resolve => setTimeout(resolve, 3000));

        let source = await page.content();
        let attempts = 0;
        const maxAttempts = 10;

        while (!source.includes('name="Password"') && attempts < maxAttempts) {
            let cfInput = await page.$('[name="cf-turnstile-response"]');
            if (cfInput) {
                const parentItem = await cfInput.evaluateHandle(element => element.parentElement);
                const coordinates = await parentItem.boundingBox();
                if (coordinates) {
                    await page.mouse.click(coordinates.x + 25, coordinates.y + (coordinates.height / 2));
                    console.log('Clicked Turnstile checkbox');
                }
            }
            await new Promise(resolve => setTimeout(resolve, 1500));
            source = await page.content();
            attempts++;
        }

        console.log('Bypass attempts completed');

        // Wait a bit more and take screenshot
        await new Promise(resolve => setTimeout(resolve, 3000));
        await page.screenshot({ path: 'rebrowser_screenshot.png', fullPage: true });
        console.log('Status Code:', status);
        console.log('Screenshot saved as rebrowser_screenshot.png');
    } catch (e) {
        console.error(e);
    } finally {
        if (browser) await browser.close();
    }
}

scrape();
Enter fullscreen mode Exit fullscreen mode

And the result:

This method has been the most consistent for me.

⭐ It works not just on challenge pages but also on sites using forced Turnstile, where the token gets verified on the backend.

You don’t need to spoof headers, rotate mouse movements, or fake screen dimensions. The stealth plugin and rebrowser Chrome do enough to pass as a normal user.

Using Scraping APIs: Scrape.do for Large-Scale Projects

If you're building something serious, anything that scrapes at volume, runs 24/7, or powers a production pipeline; you don’t want to manage browsers, proxies, or CAPTCHAs yourself.

You want something that just works.

That's why Scrape.do is the cleanest option for bypassing Cloudflare and Turnstile at scale.

You send a single API request, and it handles the rest from proxy rotation to header spoofing and CAPTCHA bypass.

And you only pay for successful results.

Here’s a working Node.js example that fetches the page, renders it, and saves the screenshot:

const axios = require('axios');
const fs = require('fs');

const token = '<your-token>';
const url = encodeURIComponent('https://scrapingtest.com/cloudflare-turnstile');
const requestURL = `https://api.scrape.do/?token=${token}&url=${url}&super=true&screenShot=true&render=true&returnJSON=true`;

axios.get(requestURL)
    .then(response => {
        if (response.status === 200) {
            const content = response.data;
            const imageB64 = content.screenShots[0].image;
            const filePath = 'bypass-scrapedo.png';
            fs.writeFile(filePath, Buffer.from(imageB64, 'base64'), err => {
                if (err) console.error(err);
            });
        } else {
            console.log(response.status);
        }
    })
    .catch(error => {
        console.error(error);
    });
Enter fullscreen mode Exit fullscreen mode

And here's the crisp screenshot:

For anything bigger than a weekend project I use Scrape.do.

If I need ULTRA-customization I prefer Selenium, but it fails here and there.

Puppeteer with rebrowser-puppeteer gives me best of both worlds.

One thing's for sure, the available Cloudflare bypass methods are dwindling and I need to add a few more to my arsenal :)

Top comments (0)