How to Bypass Cloudflare with TLS Fingerprinting in Node.js
Last updated: February 15, 2026 | Reading time: 14 min
Your Node.js scraper returns a 403. Or worse — a Cloudflare challenge page. You've tried rotating user agents, adding headers, even using headless browsers. Nothing works consistently.
The problem isn't your headers or your IP. It's your TLS fingerprint. Cloudflare identifies your scraper at the TLS handshake level — before a single HTTP header is sent. Your Node.js https module has a unique TLS fingerprint that screams "I'm not a real browser."
Bypassing Cloudflare with TLS fingerprinting requires spoofing the JA3/JA4 fingerprint that your HTTP client presents during the TLS handshake. This guide shows you exactly how, with working code.
What you'll learn:
- What TLS fingerprinting is and why Cloudflare uses it
- JA3 and JA4 fingerprint formats explained
- How to spoof browser TLS fingerprints in Node.js using got-scraping
- Session rotation strategies for sustained scraping
- Real-world example: scraping Cloudflare-protected Vinted
Table of Contents
- What Is TLS Fingerprinting?
- How Cloudflare Uses TLS Fingerprints
- JA3 and JA4: The Fingerprint Formats
- Why Standard Node.js HTTP Fails
- got-scraping: The Solution
- Building a Cloudflare-Resistant Scraper
- Session Rotation Strategies
- Real-World Example: Scraping Vinted
- Advanced Techniques
- FAQ
- Resources
What Is TLS Fingerprinting?
TLS fingerprinting is a technique that identifies HTTP clients based on the specific parameters they send during the TLS (Transport Layer Security) handshake. When your client connects to a server over HTTPS, it sends a ClientHello message containing:
- Supported cipher suites — the encryption algorithms your client supports
- TLS extensions — additional capabilities (SNI, ALPN, supported groups, etc.)
- Extension order — the sequence in which extensions appear
- Supported TLS versions — TLS 1.2, 1.3, etc.
- Elliptic curve groups — which curves your client prefers
- Signature algorithms — how your client verifies certificates
Every HTTP client — Chrome, Firefox, Safari, Node.js https, Python requests, Go net/http — sends a unique combination of these parameters. This combination is the TLS fingerprint.
The crucial insight: you can't fake a TLS fingerprint by changing HTTP headers. The fingerprint is established at the TLS layer, below HTTP. By the time your headers reach the server, Cloudflare has already identified your client type from the TLS handshake.
According to research by Salesforce (who created the JA3 method), TLS fingerprinting can identify client applications with 99.4% accuracy across billions of connections.
graph TD
A[Client connects to server] --> B[TLS ClientHello]
B --> C{Server checks<br>TLS fingerprint}
C -->|"Matches Chrome/Firefox"| D[Allow connection]
C -->|"Matches Node.js/Python"| E[Block / Challenge]
D --> F[HTTP Request proceeds]
E --> G[403 or Cloudflare page]
style E fill:#ff6b6b
style D fill:#51cf66
How Cloudflare Uses TLS Fingerprints
Cloudflare's bot detection stack operates on multiple layers. TLS fingerprinting is the first and hardest to bypass:
Layer 1: TLS Fingerprint (Pre-HTTP)
- Captures JA3/JA4 hash of the ClientHello
- Compares against known browser fingerprints
- Node.js default: immediate flag as suspicious
Layer 2: HTTP/2 Fingerprint
- Frame ordering, header compression settings, window sizes
- Different between browsers and HTTP libraries
Layer 3: HTTP Headers
- User-Agent, Accept, Accept-Language order and values
- Header order matters — browsers have consistent ordering
Layer 4: JavaScript Challenge
- Tests JavaScript execution capability
- Validates browser APIs and timing
- Only reached if Layers 1-3 pass
Most scrapers focus on Layers 3 and 4, ignoring the fact that Cloudflare has already flagged them at Layer 1. This is why rotating user agents and adding realistic headers doesn't work — your TLS fingerprint already betrayed you.
JA3 and JA4: The Fingerprint Formats
JA3 (2017)
JA3 is a method for creating a fingerprint from the TLS ClientHello. It concatenates five fields from the ClientHello, separated by commas:
JA3 = MD5(TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats)
Example JA3 for Chrome 120:
SSLVersion: 771
Ciphers: 4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53
Extensions: 0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-21
EllipticCurves: 29-23-24
EllipticCurvePointFormats: 0
JA3 Hash: cd08e31494f9531f560d64c695473da9
Example JA3 for Node.js 20 https:
JA3 Hash: eb67d79ef3a33513cce1b9a1b1084995
Cloudflare maintains a database of known JA3 hashes. cd08e31494f9531f560d64c695473da9 → Chrome → allow. eb67d79ef3a33513cce1b9a1b1084995 → Node.js → challenge or block.
JA4 (2023)
JA4 is the successor to JA3, created by FoxIO. It improves on JA3 by:
- Separating fingerprint into components (not just one hash)
- Being more resilient to minor TLS stack changes
- Supporting TLS 1.3 better
JA4 format:
JA4 = t{TLS version}d{SNI}{# of ciphers}{# of extensions}_{cipher hash}_{extension hash}
Example:
JA4: t13d1517h2_8daaf6152771_b0da82dd1658 (Chrome)
JA4: t13d1512h2_5b57614c22b0_7dc74d184f4c (Node.js)
Modern Cloudflare deployments check both JA3 and JA4.
Why Standard Node.js HTTP Fails
Node.js's built-in https module and popular libraries like axios, node-fetch, and undici all use Node.js's default TLS implementation. This creates a distinctive TLS fingerprint that doesn't match any real browser.
// This will get blocked by Cloudflare
import https from 'node:https';
const req = https.get('https://cloudflare-protected-site.com', (res) => {
// res.statusCode === 403 🚫
});
The problem is fundamental: you can't change the cipher suite order, extension list, or TLS parameters using Node.js's standard API. The tls module exposes limited configuration — nowhere near enough to replicate Chrome's ClientHello.
Even Puppeteer and Playwright have this issue in headless mode. While they run real Chromium, headless browsers have subtle TLS differences that Cloudflare can detect.
What Doesn't Work
| Approach | Why It Fails |
|---|---|
| Rotating User-Agent | Cloudflare checks TLS before reading headers |
| Adding realistic headers | Same — TLS fingerprint is checked first |
| Axios/node-fetch | Same Node.js TLS stack = same fingerprint |
| Headless Chrome | Headless-specific TLS differences detected |
| Puppeteer stealth plugin | Helps with JS challenges but not TLS layer |
| cURL from Node.js | Works but spawning processes is slow and resource-heavy |
got-scraping: The Solution
got-scraping is an HTTP client built by Apify specifically to solve the TLS fingerprinting problem. It's based on got but adds:
- Browser-like TLS fingerprints — mimics Chrome, Firefox, or Safari at the TLS layer
- Automatic header generation — realistic browser headers in correct order
- HTTP/2 fingerprint spoofing — matches browser HTTP/2 behavior
- Session management — maintains cookies and state across requests
Installation
npm install got-scraping
Basic Usage
import { gotScraping } from 'got-scraping';
const response = await gotScraping({
url: 'https://cloudflare-protected-site.com',
// Automatically uses a browser-like TLS fingerprint
});
console.log(response.statusCode); // 200 ✅
console.log(response.body); // Actual page content
That's it. got-scraping handles TLS fingerprint spoofing, header ordering, and HTTP/2 settings automatically. No configuration needed for basic usage.
How It Works Under the Hood
got-scraping uses a custom TLS layer that:
- Patches the cipher suite list to match Chrome's order
- Reorders TLS extensions to match browser patterns
- Sets ALPN protocols correctly (h2, http/1.1)
- Configures elliptic curves in browser order
- Handles GREASE (Generate Random Extensions And Sustain Extensibility) — random values Chrome adds that many scrapers miss
The result: when Cloudflare computes the JA3/JA4 hash of your connection, it matches a real Chrome browser.
graph LR
subgraph "Standard Node.js"
A1[Your Code] --> B1[node:https]
B1 --> C1[Node TLS Stack]
C1 -->|"JA3: Node.js ❌"| D1[Cloudflare]
D1 --> E1[403 Blocked]
end
subgraph "got-scraping"
A2[Your Code] --> B2[got-scraping]
B2 --> C2[Patched TLS Stack]
C2 -->|"JA3: Chrome ✅"| D2[Cloudflare]
D2 --> E2[200 OK]
end
Building a Cloudflare-Resistant Scraper
Here's a complete scraper that handles Cloudflare-protected sites with session management, retry logic, and proxy rotation:
import { gotScraping } from 'got-scraping';
class CloudflareResistantScraper {
constructor(options = {}) {
this.proxyUrl = options.proxyUrl;
this.maxRetries = options.maxRetries || 3;
this.sessionCount = 0;
this.maxRequestsPerSession = options.maxRequestsPerSession || 75;
this.requestCount = 0;
}
async request(url, options = {}) {
if (this.requestCount >= this.maxRequestsPerSession) {
this.rotateSession();
}
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
try {
const response = await gotScraping({
url,
proxyUrl: this.proxyUrl,
useHeaderGenerator: true,
headerGeneratorOptions: {
browsers: ['chrome'],
operatingSystems: ['macos', 'windows'],
locales: ['en-US', 'en-GB', 'fr-FR', 'de-DE'],
},
timeout: { request: 30000 },
retry: { limit: 0 }, // We handle retries ourselves
...options,
});
this.requestCount++;
if (response.statusCode === 403 || response.statusCode === 503) {
console.log(`Cloudflare challenge on attempt ${attempt + 1}`);
this.rotateSession();
continue;
}
return response;
} catch (error) {
console.error(`Request failed (attempt ${attempt + 1}):`, error.message);
if (attempt === this.maxRetries - 1) throw error;
await this.delay(2000 * (attempt + 1)); // Exponential backoff
}
}
}
rotateSession() {
this.sessionCount++;
this.requestCount = 0;
console.log(`Rotated to session #${this.sessionCount}`);
// If using proxies, rotate the proxy here
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage
const scraper = new CloudflareResistantScraper({
proxyUrl: 'http://user:pass@proxy.example.com:8080',
maxRequestsPerSession: 50,
});
const response = await scraper.request('https://www.vinted.fr/catalog?search_text=nike');
console.log(`Status: ${response.statusCode}`);
Key Design Decisions
Session rotation every 50-100 requests — Cloudflare tracks behavior per session. Too many requests from one "browser session" triggers detection.
Residential proxies — Datacenter IPs are largely blocked. Residential proxies (from real ISPs) have much higher success rates with Cloudflare.
Randomized browser profiles —
headerGeneratorOptionsrotates between different Chrome versions, operating systems, and locales to avoid fingerprint consistency.Exponential backoff — If Cloudflare challenges you, wait longer before retrying. Aggressive retries increase detection risk.
Session Rotation Strategies
Session rotation is critical for sustained Cloudflare scraping. Here are three strategies ranked by effectiveness:
Strategy 1: Request Count-Based (Simple)
Rotate after a fixed number of requests:
const MAX_REQUESTS = 75;
let requestCount = 0;
async function makeRequest(url) {
if (requestCount >= MAX_REQUESTS) {
// Reset cookies, rotate proxy, new browser fingerprint
requestCount = 0;
}
requestCount++;
return gotScraping({ url });
}
Pros: Simple, predictable. Cons: Cloudflare can detect regular patterns.
Strategy 2: Time-Based with Jitter (Better)
Rotate at random intervals:
function getNextRotation() {
// Rotate every 3-8 minutes with random jitter
return (3 + Math.random() * 5) * 60 * 1000;
}
Pros: Less predictable. Cons: May waste sessions on quiet periods.
Strategy 3: Adaptive (Best)
Monitor response codes and rotate on signs of detection:
async function adaptiveRequest(url) {
const response = await gotScraping({ url });
if (response.statusCode === 429 || response.statusCode === 503) {
// Detected — rotate immediately
rotateSession();
await delay(5000 + Math.random() * 5000);
return adaptiveRequest(url); // Retry with new session
}
if (response.body.includes('challenge-platform')) {
// Cloudflare JS challenge — rotate and wait
rotateSession();
await delay(10000);
return adaptiveRequest(url);
}
return response;
}
Recommended: Use Strategy 3 (adaptive) with Strategy 1 (count-based) as a fallback. Rotate after 50-100 requests OR whenever you detect a Cloudflare challenge, whichever comes first.
Real-World Example: Scraping Vinted
Vinted uses Cloudflare's full protection stack — TLS fingerprinting, JavaScript challenges, and behavioral analysis. It's one of the harder sites to scrape, which makes it a perfect test case.
The Vinted Smart Scraper on Apify implements all the techniques described in this guide:
- got-scraping for TLS fingerprint spoofing
- Residential proxy rotation with session management (50-100 requests per session)
- Cookie persistence within sessions for state management
- Adaptive rate limiting — backs off when Cloudflare responds with challenges
The scraper handles all of this internally — you provide a search query and get structured JSON results in 12 seconds. No proxy configuration, no fingerprint management, no session rotation logic needed on your end.
// Using Vinted Smart Scraper via Apify API
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('kazkn/vinted-smart-scraper').call({
search: 'Nike Air Force 1',
country: 'fr',
sort: 'price_low_to_high',
limit: 50,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(`Found ${items.length} listings`);
items.forEach(item => {
console.log(`${item.title} — €${item.price} (${item.condition})`);
});
If you want the raw scraping experience, read our Vinted scraper tutorial. If you want AI-powered queries, check the Vinted MCP Server.
🎯 Skip the Cloudflare headaches
Vinted Smart Scraper handles TLS fingerprinting, proxies, and session rotation for you. $5 free monthly credits.
Advanced Techniques
HTTP/2 Fingerprinting
Beyond TLS, Cloudflare also analyzes HTTP/2 settings. Browsers have specific:
- SETTINGS frame values (HEADER_TABLE_SIZE, MAX_CONCURRENT_STREAMS, etc.)
- WINDOW_UPDATE sizes
- Priority frame ordering
got-scraping handles most HTTP/2 fingerprint spoofing, but for maximum stealth:
const response = await gotScraping({
url: 'https://target.com',
http2: true, // Force HTTP/2
headerGeneratorOptions: {
browsers: ['chrome'],
browserVersionRange: { chrome: [120, 125] }, // Specific version range
},
});
Canvas and WebGL Fingerprinting (JavaScript Level)
If you pass TLS and HTTP/2 checks but still get JavaScript challenges, Cloudflare may be checking browser-level fingerprints:
- Canvas fingerprint — rendering differences between browsers/GPUs
- WebGL renderer — GPU identification
- Audio context — audio processing fingerprint
These require full browser automation (Puppeteer/Playwright) rather than HTTP-level tools. For most scraping tasks, TLS fingerprint spoofing with got-scraping is sufficient.
GREASE Extensions
Chrome adds random GREASE (Generate Random Extensions And Sustain Extensibility) values to TLS ClientHello messages. These are deliberately invalid extension types that well-behaved servers ignore. Cloudflare checks for their presence — if your client doesn't send GREASE values, it's not Chrome.
got-scraping includes GREASE support by default. If building a custom solution, ensure your TLS stack sends GREASE values in the correct positions.
FAQ
What is a JA3 fingerprint?
A JA3 fingerprint is an MD5 hash computed from five fields of a TLS ClientHello message: TLS version, cipher suites, extensions, elliptic curves, and point formats. Created by Salesforce's research team, it uniquely identifies HTTP clients with 99.4% accuracy. Cloudflare uses JA3 (and its successor JA4) to distinguish real browsers from scrapers.
Does got-scraping work for all Cloudflare-protected sites?
got-scraping works for most Cloudflare-protected sites that rely on TLS fingerprinting as their primary detection method. Sites with aggressive JavaScript challenges (Cloudflare's "Under Attack" mode) may require additional measures like full browser automation. For Vinted, Amazon, and most e-commerce sites, got-scraping is sufficient.
Is TLS fingerprint spoofing legal?
TLS fingerprint spoofing itself is a technical method, not inherently illegal. Legality depends on what you're scraping and the jurisdiction. Scraping publicly available data is generally legal in the EU (CJEU rulings) and the US (hiQ v LinkedIn). Always review the target site's Terms of Service.
How many requests can I make per session before rotating?
For Cloudflare-protected sites, 50-100 requests per session is the safe range. Some sites allow more, others flag you after 30 requests. Start with 50, monitor for 403/503 responses, and adjust. The Vinted Smart Scraper uses adaptive session rotation based on response monitoring.
Can I use free proxies for Cloudflare bypass?
Free proxies almost never work for Cloudflare bypass. They're datacenter IPs that are already blocklisted. You need residential proxies — IPs from real ISPs. Services like Bright Data, Oxylabs, or Apify's built-in proxy provide residential IPs. The Vinted Smart Scraper includes residential proxy rotation in its pricing.
Why does Puppeteer stealth plugin still get blocked?
Puppeteer stealth (puppeteer-extra-plugin-stealth) patches JavaScript-level detection but doesn't change the underlying TLS fingerprint. Headless Chromium has a different TLS fingerprint than headed Chrome. Cloudflare detects this difference at the TLS layer before any JavaScript runs. Combine Puppeteer with a TLS proxy or use got-scraping for HTTP-level scraping.
What's the difference between JA3 and JA4?
JA3 (2017) creates a single MD5 hash from TLS ClientHello parameters. JA4 (2023) creates a multi-part fingerprint with more detail: TLS version, SNI, cipher count, extension count, plus separate hashes for ciphers and extensions. JA4 is more robust against evasion techniques and better handles TLS 1.3. Cloudflare now checks both.
How does got-scraping differ from curl-impersonate?
Both tools spoof TLS fingerprints. curl-impersonate is a modified cURL binary — faster but requires spawning a system process from Node.js (slow, resource-heavy). got-scraping is a native Node.js library — integrates directly into your code, supports async/await, and includes automatic header generation. For Node.js projects, got-scraping is the better choice.
Can Cloudflare detect got-scraping?
Cloudflare continuously updates detection methods. As of February 2026, got-scraping with residential proxies and session rotation successfully bypasses Cloudflare on the vast majority of protected sites. However, no tool guarantees 100% bypass forever — it's an ongoing arms race. Using managed solutions like the Vinted Smart Scraper ensures you benefit from ongoing updates without maintaining the bypass logic yourself.
Resources
- got-scraping GitHub — The library behind the TLS bypass
- JA3 by Salesforce — Original JA3 research paper
- JA4+ by FoxIO — JA4 specification
- Apify anti-blocking guide — Comprehensive anti-bot bypass strategies
- Vinted Smart Scraper — Production implementation of these techniques
Related articles:
- Vinted Scraper: How to Extract Listing Data Automatically in 2026
- Best Vinted Scraper Tools Compared: 2026 Guide
- Web Scraping Without Code: 5 Best No-Code Scrapers
- Vinted Price Tracker: Build Automated Price Monitoring
- How to Use Vinted Data in Claude, Cursor, and Any AI Tool
- I Built an MCP Server for Vinted
- App Store Scraper: Find Untranslated iOS Apps
Built with Vinted Smart Scraper | Vinted MCP Server | GitHub | npm
Top comments (0)