OmniScrapeOmniScrape
ProductsSolutionsGuidesDocs ↗PricingAbout
← All guides
Anti-Bot Bypass

How to Bypass Cloudflare in Node.js

The same script works against your staging site and returns "Just a moment..." against production the moment Cloudflare goes on. You swap axios for got, then for native fetch, then add every header the browser sends. The response does not change.

Node's HTTP clients all sit on the same TLS implementation, and that implementation announces itself during the handshake — before headers exist. Moving between HTTP libraries changes the API surface and nothing that Cloudflare scores. This guide covers what does change the outcome in Node, what puppeteer-extra-stealth genuinely covers, and where the remaining detection surface lives.

On this page

1. One TLS stack under every Node HTTP client2. What puppeteer-extra-stealth covers, and what it does not3. CDP is a detection surface of its own4. Calling OmniScrape from Node5. Concurrency is a plan limit, not a tuning dial6. FAQ

1.One TLS stack under every Node HTTP client

axios, got, node-fetch, and the built-in fetch are all conveniences over Node's networking layer, which negotiates TLS through Node's bundled OpenSSL with Node's own defaults. The cipher ordering, extension set, and supported groups that results from that configuration is consistent across those libraries and distinct from any browser's.

Cloudflare hashes those handshake properties into a JA3 or JA4 value at the edge. On a zone that scores fingerprints, the decision is available before the request line is parsed. This is why the header-tuning phase of debugging produces no signal: you are editing something the edge has not looked at yet.

Node also exposes fewer knobs here than people expect. You can adjust some TLS options, but you cannot reproduce Chrome's GREASE behaviour and full extension ordering from userland. Getting a browser fingerprint in Node realistically means running a browser.

2.What puppeteer-extra-stealth covers, and what it does not

The stealth plugin is a collection of evasions for known automation tells: navigator.webdriver, inconsistencies in the plugins and mimeTypes arrays, permissions query behaviour, WebGL vendor and renderer strings, Chrome-specific runtime objects missing under automation, and a number of similar surface details. Against naive checks it is effective, and it is a reasonable default if you are running Puppeteer anyway.

It patches the JavaScript-visible surface. It does not change how the browser behaves under behavioural analysis — mouse movement, scroll cadence, timing between interactions — and it does not change the fact that a headless environment differs from a desktop one in ways that are measurable without touching any of the patched properties.

The deeper issue is that the evasion list is a snapshot. Each entry exists because someone found a detection vector; new vectors keep being found, and the plugin trails them. A stack that passes today is not a stack that passes indefinitely, and the degradation is gradual rather than obvious.

3.CDP is a detection surface of its own

Puppeteer and Playwright both drive Chrome through the Chrome DevTools Protocol. Attaching a CDP client changes observable characteristics of the runtime, and detection scripts have historically probed for those characteristics directly rather than for the properties the stealth plugin patches.

This is why "I added stealth and it still fails" is such a common report. The evasions addressed the properties they were written for; the check that caught you was looking at the automation channel itself. The two are different layers, and patching one does not cover the other.

Running Chrome in headful mode inside a virtual display removes some headless-specific signals at the cost of substantially more resource usage per instance. It is a real option for hard targets, and it is also the point at which the infrastructure question — how many browsers, supervised how, recycled how often — becomes the actual engineering problem rather than a detail.

4.Calling OmniScrape from Node

The API is a single POST with no SDK requirement, so native fetch is sufficient. Send the URL with mode auto and enable_solver true; the fast HTTP attempt and the browser escalation are decided server-side based on whether challenge signals appear in the first response.

Read your key from the environment. Check success on the envelope, then data.status_code for what the origin actually returned, then assert something about the body — a length floor, or the presence of a selector you expect. Envelope success plus an origin 404 is a perfectly ordinary outcome and should not enter your dataset as content.

For long or stateful flows — authenticated sessions, multi-step wizards, anything where you already own a working Puppeteer or Playwright script — Browser-as-a-Service is the closer fit. You connect over CDP to a remote browser and keep your script unchanged, rather than reimplementing the flow as a sequence of scrape calls.

Native fetch against POST /v1/scrape
javascript
123456789101112131415161718192021222324252627282930const API = "https://api.omniscrape.io/v1/scrape";

async function scrape(url, overrides = {}) {
  const res = await fetch(API, {
    method: "POST",
    headers: {
      "X-API-Key": process.env.OMNISCRAPE_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      url,
      mode: "auto",
      enable_solver: true,
      output_format: "html",
      ...overrides,
    }),
  });

  if (!res.ok) throw new Error(`HTTP ${res.status}`);

  const body = await res.json();
  if (!body.success) throw new Error(body.error ?? "unlock failed");
  return body;
}

const result = await scrape("https://cf-protected-shop.com/product/8821", {
  proxy: "residential:us",
});

console.log(result.metadata.challenge_solved, result.data.status_code);
Fixed worker pool instead of an unbounded Promise.all
javascript
1234567891011121314151617181920212223242526272829303132333435363738394041const CONCURRENCY = 5;               // match your plan's limit
const RETRYABLE = new Set([429, 500, 502, 503, 504]);

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function scrapeWithRetry(url, attempts = 4) {
  for (let i = 0; i < attempts; i += 1) {
    const res = await fetch(API, {
      method: "POST",
      headers: {
        "X-API-Key": process.env.OMNISCRAPE_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ url, mode: "auto", enable_solver: true }),
    });

    if (RETRYABLE.has(res.status)) {
      // 429 responses carry Retry-After; otherwise back off exponentially.
      const wait = Number(res.headers.get("retry-after")) || 2 ** i;
      await sleep(wait * 1000);
      continue;
    }
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  }
  return null;
}

async function scrapeAll(urls) {
  const queue = [...urls];
  const out = [];
  const workers = Array.from({ length: CONCURRENCY }, async () => {
    while (queue.length) {
      const url = queue.shift();
      const r = await scrapeWithRetry(url);
      if (r) out.push(r);
    }
  });
  await Promise.all(workers);
  return out;
}

5.Concurrency is a plan limit, not a tuning dial

A common Node mistake is mapping an array of URLs straight into Promise.all. That opens every request at once, which exceeds your plan's concurrency limit and returns 429 with a CONCURRENCY_LIMIT code. Retrying those immediately produces a retry storm that keeps the limit saturated.

A fixed worker pool sized to the plan — 5 on Pay-As-You-Go, 10 on Startup, 25 on Growth — keeps throughput at the ceiling without ever tripping it. The pattern above drains a shared queue from a fixed number of workers, which is both simpler and better behaved than unbounded parallelism with retries bolted on.

Honour Retry-After when it is present. The 429 response includes it precisely so clients do not have to guess, and guessing shorter is how a transient limit turns into a sustained one.

Frequently asked questions

Does switching from axios to got or native fetch help?

No. All of them use Node's TLS layer and present the same handshake fingerprint. The library choice affects your API ergonomics and nothing Cloudflare scores at the edge.

Is puppeteer-extra-stealth enough on its own?

Sometimes, and less often over time. It patches known JavaScript-visible automation tells, but it does not address behavioural analysis or detection that probes the CDP channel itself. Treat it as a maintained dependency, not a solved problem.

Why does my Puppeteer script fail even with stealth enabled?

Most often because the check looked at something stealth does not cover — the automation channel, headless-specific environment characteristics, or interaction timing. Those are different layers from the properties the plugin patches.

Should I use the Scrape API or Browser-as-a-Service?

Use the Scrape API for fetch-and-parse work, including single-page challenges. Use BaaS when you already have a Puppeteer or Playwright script with real state — logins, multi-step flows, long sessions — and want to keep it unchanged while running it on a remote browser over CDP.

How do I avoid 429 responses?

Cap in-flight requests to your plan's concurrency with a fixed worker pool rather than mapping into Promise.all, and honour the Retry-After header when a 429 does occur. Unbounded parallelism plus immediate retry is what turns a brief limit into a sustained one.

Related guides

  • How to Bypass Cloudflare When Web Scraping
  • How to Bypass Cloudflare in Python
  • Puppeteer Web Scraping: Patterns, Anti-Bot Limits, and BaaS Integration
  • Web Scraping with Node.js: fetch, Cheerio, and the OmniScrape API
  • cloudscraper and cfscrape Alternatives That Still Work
  • How to Bypass DataDome When Web Scraping

Ready to scrape without blocks?

Published unit rates, clear plan limits, and billing tied to successful work. No hidden fees and no guessing what a scrape will cost. Free trial credit on signup. No credit card required.

Ready to get started?

Start scraping protected sites today. No credit card required.

OmniScrapeOmniScrape

Web scraping infrastructure for developers. One API call to bypass any protection.

All systems operational

Payments accepted

Credit / Debit CardVisaMastercardCryptoBTCUSDTETH50+ coins

Product

  • Web Unlocker
  • Browser-as-a-Service
  • Residential Proxies
  • Pricing

Developers

  • API Reference ↗
  • Quickstart ↗
  • All Guides
  • Use Cases
  • Status

Company

  • About
  • Contact

Legal

  • Privacy Policy
  • Terms of Service
  • Refund Policy
  • Cookie Policy
  • Acceptable Use

Popular guides

  • How to Bypass Cloudflare When Web Scraping
  • How to Bypass DataDome When Web Scraping
  • How to Bypass Akamai Bot Manager When Web Scraping
  • How to Bypass F5 BIG-IP Bot Defense When Web Scraping
  • Web Scraping API: Endpoint, Modes, Output Formats & Integration Patterns
  • Web Scraping with Python
  • Scrape JavaScript-Rendered Pages: SPAs, Hydration, and Hidden APIs
  • Web Scraping Without Getting Blocked
  • Headless Browser Scraping: When to Use It and How to Do It Right
  • Rotating Proxies for Web Scraping: Policies, Session Binding, and Geo Pools
  • OmniScrape vs ScrapingBee
  • OmniScrape vs ZenRows
Browse all guides →

OmniScrape is a product of PT Data Digital Grup. Copyright ©2026.

PrivacyTermsRefundsAcceptable Use