What Is Scrapy Fingerprinting?
Scrapy fingerprinting is the set of signals — HTTP headers, TLS handshake data, request timing, and JavaScript behavior — that anti-bot systems like Cloudflare, DataDome, and Akamai use to identify Scrapy spiders as bots rather than real browsers. Because Scrapy sends raw HTTP requests instead of rendering a page the way Chrome or Firefox does, its default fingerprint is easy to flag. Fixing it means matching headers, TLS, timing, and JavaScript execution to what a genuine browser produces.

By 2026, bot-management vendors can flag a Scrapy request within milliseconds of the TCP handshake completing — long before your spider’s parse() method ever runs. They don’t need to inspect your code; the fingerprint alone gives you away. Understanding which signals expose a spider, and which are worth spoofing for a given target, is what separates a scraper that returns clean data from one stuck looping on 403 and 429 responses.
How Websites Fingerprint Scrapy Requests
Detection happens in layers, and a site’s anti-bot vendor typically checks several at once rather than relying on any single signal.
Layer 1: HTTP Headers
This is the simplest layer to check and the first one most anti-bot systems evaluate. Scrapy’s out-of-the-box headers are a giveaway:
# Default Scrapy User-Agent — flagged instantly
User-Agent: Scrapy/2.11.0 (+https://scrapy.org)
# Headers a real Chrome request always sends, that Scrapy omits by default
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
A request missing the Sec-Fetch-* family, or shipping the literal string “Scrapy” in its User-Agent, gets filtered before it reaches any deeper check.
Layer 2: TLS Fingerprinting (JA3/JA4)
This is the layer most Scrapy users never think about. Before a single byte of HTTP is exchanged, the TLS handshake itself reveals which library made the connection:
- JA3 hash — a fingerprint built from the cipher suites, TLS extensions, and elliptic curves your client offers.
- HTTP/2 fingerprint — the SETTINGS frame, window size, and stream priority tree.
- Python’s default TLS stack (used by Twisted, which powers Scrapy) produces a JA3 hash that matches no real browser release.
| Client | JA3 Hash (illustrative) | Detection Result |
|---|---|---|
| Chrome 120 | cd08e31494f9531f560d64c695473da9 | Looks like a browser |
| Firefox 121 | b32309a26951912be7dba376398abc3b | Looks like a browser |
| Python / Twisted (Scrapy default) | bf09a91d7f0e10a4a0b69e8f5c2d7a32 | Flagged as automated traffic |
Layer 3: Behavioral Fingerprinting
- Request timing — Scrapy fires requests at machine speed; humans pause, scroll, and re-read.
- Navigation pattern — Scrapy typically crawls breadth-first or depth-first in a predictable order; humans jump around.
- Missing assets — Scrapy never fetches the CSS, images, fonts, or JS a real page load pulls in.
- Cookie handling — Scrapy’s default cookie jar doesn’t replicate how a browser accumulates and replays cookies across a session.
- Referrer chains — spiders often jump straight to a deep URL with no referrer, something a browsing human rarely does.
Layer 4: JavaScript Fingerprinting
Because Scrapy never executes JavaScript, it automatically fails any challenge that depends on script execution — which is most modern anti-bot challenges. Sites check for canvas rendering output, run WebGL fingerprinting probes against the GPU, and layer in canvas fingerprinting checks that hash how your (non-existent) rendering engine draws a hidden shape. They also check automation-specific markers like navigator.webdriver and injected driver properties — the same signals covered in this guide to browser fingerprinting and automation detection. Plain Scrapy can’t produce any of these signals at all, which is itself a tell.
Anti-Fingerprinting Techniques for Scrapy
1. Send Realistic Browser Headers
# settings.py — realistic default headers
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Cache-Control': 'max-age=0',
'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
}
# Rotate User-Agents to match your headers
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
'scrapy_fake_useragent.middleware.RandomUserAgentMiddleware': 400,
}
2. Spoof Your TLS Fingerprint
Headers alone don’t fix Layer 2. To change the JA3 hash itself, swap Scrapy’s default HTTP client for a library that impersonates a real browser’s TLS stack:
# pip install curl_cffi
from curl_cffi.requests import Session
class TLSSpoofMiddleware:
def process_request(self, request, spider):
session = Session(impersonate="chrome120")
response = session.get(
request.url,
headers=dict(request.headers),
timeout=30
)
return HtmlResponse(
url=request.url,
status=response.status_code,
headers=dict(response.headers),
body=response.content,
request=request
)
3. Add Human-Like Timing
# settings.py
DOWNLOAD_DELAY = 2
RANDOMIZE_DOWNLOAD_DELAY = True
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 3
AUTOTHROTTLE_MAX_DELAY = 10
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
4. Rotate Residential Proxies
import random
class RotatingProxyMiddleware:
def __init__(self):
self.proxies = [
'http://user:[email protected]:8080',
'http://user:[email protected]:8080',
'http://user:[email protected]:8080',
]
def process_request(self, request, spider):
request.meta['proxy'] = random.choice(self.proxies)
Proxy quality matters as much as rotation frequency — a residential IP behind a stale TLS fingerprint still gets flagged just as fast as a datacenter IP.
5. Render JavaScript With Scrapy-Playwright
# pip install scrapy-playwright
DOWNLOAD_HANDLERS = {
"http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
"https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
class JSSpider(scrapy.Spider):
def start_requests(self):
yield scrapy.Request(
url='https://target-site.com',
meta={
'playwright': True,
'playwright_include_page': True,
}
)
async def parse(self, response):
page = response.meta['playwright_page']
await page.close()
yield {'title': response.css('h1::text').get()}
Advanced Anti-Detection Strategy
Keep Every Signal Internally Consistent
A common mistake is randomizing fingerprint elements independently — a random User-Agent paired with a fixed JA3 hash, for example. Anti-bot systems specifically look for mismatches like these:
| Check | What Must Match | What Gets Flagged |
|---|---|---|
| User-Agent + TLS | A Chrome UA needs a Chrome-shaped JA3 | Python JA3 behind a Chrome UA |
| User-Agent + Client Hints | Sec-Ch-Ua version must match the UA string |
Chrome/120 UA with Chrome/118 hints |
| Language + Timezone + IP | A US proxy should carry en-US and a US timezone |
Tokyo IP with US locale settings |
| Platform + Screen | A mobile UA needs a mobile-sized viewport | Mobile UA reporting a 1920×1080 screen |
Scrape Through Sessions, Not Isolated Requests
- Visit the homepage before jumping to a product or listing page.
- Load a search page before requesting its results, rather than guessing the results URL directly.
- Send referrer headers that match the navigation path you’re simulating.
- Accept and replay cookies naturally across the session instead of dropping them per request.
Common Mistakes That Undo All This Hardening
Most Scrapy spiders that still get caught aren’t failing because a team skipped anti-fingerprinting entirely — they’re failing because one piece of the setup contradicts another. A few patterns show up again and again in real crawler codebases:
- Rotating the User-Agent but not the TLS fingerprint. A request claiming to be Chrome 120 with a Python-shaped JA3 hash is a bigger red flag than an honest Scrapy User-Agent, because it signals deliberate spoofing rather than a default configuration.
- Reusing one proxy pool across wildly different target sites. If the same IP block hits ten different domains in a short window with an identical header fingerprint, shared anti-bot vendors (Cloudflare and DataDome both serve many customers) can correlate the pattern across sites, not just within one.
- Setting
DOWNLOAD_DELAYbut ignoring crawl order. A perfectly randomized delay between requests still looks robotic if every request follows the exact same breadth-first path through a site’s URL structure. - Testing against the wrong reference. Checking your fingerprint against browserleaks.com confirms your headers look plausible in isolation, but says nothing about whether they’re internally consistent with your TLS handshake — you need both checks, not just one.
- Assuming Scrapy-Playwright makes a spider undetectable. Rendering JavaScript closes the biggest gap, but a Playwright-driven browser still runs at a fixed viewport, with no mouse movement or scroll behavior, unless you deliberately add it — sites doing deep behavioral analysis can still tell.
The fix for all five is the same principle: treat your fingerprint as one coherent identity, not a checklist of independent settings to tick off.
Scrapy vs Headless Browser vs Isolated Browser Profile
| Factor | Scrapy (HTTP-only) | Scrapy + Playwright | Isolated Browser Profile |
|---|---|---|---|
| Speed | Very fast — 1,000s concurrent | Moderate — 10s to 100s concurrent | Slowest — 1s to 10s concurrent |
| Resource usage | Minimal | Moderate (headless browser per request) | Highest (full browser session) |
| TLS fingerprint | Python — detectable | Real browser engine | Real browser engine |
| JavaScript execution | None | Full | Full |
| Anti-detection strength | Basic — headers only | Good | Strongest — consistent, persistent fingerprint |
For the small number of targets where even Scrapy-Playwright gets caught — usually sites layering behavioral analysis on top of JS challenges — teams often move to genuinely isolated antidetect browser profiles instead of trying to spoof a headless session from scratch every run. A profile with a persistent, consistent fingerprint across visits reads as a returning user rather than a fresh bot each time.
How to Test Your Scraper’s Fingerprint
- bot.sannysoft.com — flags common automation indicators.
- browserleaks.com — a full fingerprint readout of whatever client hits the page.
- ja3er.com — shows the JA3 hash your connection produced.
- httpbin.org/headers — echoes back exactly what headers you sent.
- pixelscan.net — checks fingerprint signals for internal consistency, not just presence.
import scrapy
import json
class FingerprintTestSpider(scrapy.Spider):
name = 'fingerprint_test'
start_urls = ['https://httpbin.org/headers']
def parse(self, response):
headers = json.loads(response.text)['headers']
for key, value in headers.items():
self.logger.info(f'{key}: {value}')
expected = ['Accept', 'Accept-Language', 'Sec-Fetch-Dest', 'Sec-Ch-Ua', 'Sec-Fetch-Mode']
for h in expected:
if h not in headers:
self.logger.warning(f'Missing header: {h}')
🏆 Send.win Verdict
Scrapy fingerprinting is ultimately a battle over which layers you can afford to spoof and which ones you can’t. For the handful of targets where header tricks, TLS spoofing, and even Scrapy-Playwright still get caught, the fix usually isn’t more scraper engineering — it’s running the request through a genuinely persistent browser session instead. Send.win’s Automation API, included on the Pro plan, lets you drive real Sendwin Browser sessions with Playwright or Puppeteer, so your traffic carries an authentic, consistent fingerprint instead of a spoofed one that has to be rebuilt on every request.
Try Send.win free for 30 days — no credit card required.
Frequently Asked Questions
Why does my Scrapy spider get blocked even with rotating proxies?
Proxies only change your IP fingerprint. If your TLS fingerprint (JA3), header set, and request timing still look automated, anti-bot systems will block you regardless of which IP you’re using. Proxies fix one layer out of four.
Is plain Scrapy good enough for heavily protected sites?
Usually not. Against Cloudflare, DataDome, or PerimeterX, raw HTTP requests get caught at the TLS or JavaScript layer no matter how good your headers are. You need Scrapy-Playwright for real JS rendering and a genuine browser TLS fingerprint, or a TLS-spoofing library like curl_cffi.
Can I change Scrapy’s JA3 fingerprint directly?
Not through settings alone — JA3 is determined by the TLS library underneath (OpenSSL via Twisted, in Scrapy’s case). To change it you need curl_cffi with browser impersonation, or Scrapy-Playwright, which performs the TLS handshake through an actual browser engine.
What are the clearest signs my requests are being fingerprinted?
Consistent 403 or 429 responses despite rotating IPs, CAPTCHAs on every request rather than occasional ones, JavaScript-challenge pages instead of real content, or a noticeably thinner page than what a browser sees at the same URL.
Should I switch to headless Chrome instead of Scrapy entirely?
For heavily protected targets, a headless browser has a real TLS fingerprint and executes JavaScript, which looks far more like genuine traffic. But Scrapy is dramatically faster and lighter for sites with no serious anti-bot layer. Most production crawlers run both — Scrapy for easy targets, Scrapy-Playwright (or a full browser) for protected ones.
Does adding delays alone fix behavioral fingerprinting?
It helps but doesn’t fully solve it. Random delays address timing, but predictable crawl order, missing asset requests, and unnatural referrer chains still stand out. Session-based scraping — following the same navigation path a human would — closes most of that gap.
Is it worth combining curl_cffi with Scrapy-Playwright?
Usually not necessary — they solve the same TLS problem in different ways. curl_cffi is lighter and faster for sites that just need a believable TLS handshake and headers; Scrapy-Playwright is the better call when the target also runs a JavaScript challenge that curl_cffi alone can’t pass.
Conclusion
Mastering scrapy fingerprinting means addressing every layer a site can check: headers, TLS, timing, and JavaScript execution. Realistic headers and delays handle the easy targets. TLS spoofing with curl_cffi or full rendering with Scrapy-Playwright handles the harder ones. For the smallest set of the most heavily protected targets, a persistent, isolated browser profile is often the only approach that consistently holds up over repeated visits. There’s no single fix — match the technique to how aggressively the target is actually checking you.