DEV Community

Greta
Greta

Posted on

How Anti-Bot Systems Actually Detect Your Scraper: The 6 Signals That Matter

How Anti-Bot Systems Actually Detect Your Scraper: The 6 Signals That Matter

Your scraper works perfectly at 10 requests and dies at 1,000. The code didn't change. The site didn't change. What changed is that you crossed a threshold in a scoring system you can't see.

Anti-bot platforms (Cloudflare, Akamai, DataDome, PerimeterX) don't block you for a single mistake. They accumulate a risk score across every request, and when it crosses a line, the responses quietly degrade — first slower, then CAPTCHAs, then 403s. Understanding the signals that feed that score is the difference between guessing and engineering.

Here are the six signals that matter, ordered by how fast they get you flagged.

Signal 1: IP Reputation — Where Your Request Comes From

The first and cheapest check. Every anti-bot vendor maintains IP intelligence that classifies addresses before your request is even parsed:

  • Datacenter IPs (AWS, Hetzner, OVH ranges) — instantly suspicious. Real humans don't browse from server racks.
  • Residential IPs from real ISPs — neutral starting score, because real customers use them.
  • Flagged residential IPs — compromised by abuse in the past, scored somewhere in between.

The uncomfortable detail: it's not just where the IP is, but what kind. A US datacenter IP hitting a German shop is a double mismatch — wrong type AND wrong geography. Conversely, a residential IP in the right country with consistent locale headers is nearly indistinguishable from a local customer. This is why geo-targeted residential proxies are the backbone of serious scraping infrastructure — the request profile and the IP profile have to agree.

You can test this yourself:

import requests

# Your raw IP, unproxied
print(requests.get("https://httpbin.org/ip", timeout=10).json())

# Through a datacenter proxy vs a residential proxy — same code, different scores
Enter fullscreen mode Exit fullscreen mode

Signal 2: TLS Fingerprint — How Your Handshake Looks

This one surprises people. Before a single HTTP byte is exchanged, your TLS ClientHello already identifies your client. Python's requests library, Chrome, Firefox, and curl each produce a distinctive fingerprint (the cipher suites they offer, their order, the extensions they send).

Anti-bot systems compare your declared User-Agent against your TLS fingerprint. Claiming to be Chrome in the headers while shaking hands like Python is a direct contradiction — and contradictions are what scoring systems are built to catch.

Practical mitigations, in increasing order of effort:

  • Use requests with honest headers (mismatch, but at least consistent elsewhere)
  • Use curl_cffi or tls-client, which can impersonate Chrome's TLS fingerprint
  • Drive a real browser (Playwright) when the target is strict — expensive but truthful

Signal 3: Behavioral Timing — The Rhythm of Your Requests

Humans are irregular. Bots are metronomes.

# The fingerprint of a bot:
for url in urls:
    scrape(url)
    time.sleep(1.0)          # exactly 1.0s, every time, forever
Enter fullscreen mode Exit fullscreen mode
# The rhythm of a person with a coffee:
for url in urls:
    scrape(url)
    time.sleep(random.uniform(1.5, 4.0))   # jitter
Enter fullscreen mode Exit fullscreen mode

Perfectly uniform intervals, 24/7 activity with no nights or weekends, zero scroll/pointer events on rendered pages — each of these is a scoring input. Randomized jitter is the cheapest fix in this entire article.

Signal 4: Session Consistency — Whether Your Story Adds Up

A real user's session has a shape: arrive, browse a few pages, maybe log in, click deeper. Your scraper's session is often: log in, hit 500 product pages back-to-back, log out. No human does that.

Two classic tells:

  • IP rotation mid-session. You rotate to a fresh IP for every request, log in successfully, then get kicked — because to the site, "the user's IP changed mid-login," which basically never happens to real people. Sticky sessions (same IP for the session's life, typically up to 30 minutes) solve this.
  • Missing browsing prelude. Landing directly on page 47 of search results with no referer, no prior page views. A quick harmless request to the homepage before the deep target adds a bit of session shape for one extra request.

Signal 5: Header and Fingerprint Coherence

Browsers send dozens of headers in specific combinations: Accept, Accept-Language, Accept-Encoding, sec-ch-ua, sec-fetch-*, cookies with consistent attributes. Scrapers typically send four headers and a User-Agent copied from a blog post in 2021.

The individual values matter less than the coherence: does this request look like it came from the browser it claims to be? curl default headers alongside a Chrome UA is another contradiction for the scoring engine.

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Upgrade-Insecure-Requests": "1",
}
Enter fullscreen mode Exit fullscreen mode

Keep the header set consistent with your IP's geography too — Accept-Language: fr-FR from a Tokyo IP is a mismatch unless you're deliberately testing mismatch behavior.

Signal 6: Historical Behavior — Your IP's Rap Sheet

The final signal is time itself. Even a perfect request profile gets scored on history: how many requests has this IP made today? How many distinct sessions? Has this fingerprint been seen being blocked before?

This is why fresh residential IPs from a large pool outperform any single IP you reuse: each request starts with a clean rap sheet. It's also why "works for a week then dies" is the standard datacenter-IP lifecycle — the score accumulates until it doesn't.

The Meta-Lesson

Notice the pattern across all six signals: anti-bot systems don't detect scraping. They detect inconsistency. Datacenter IP claiming to be a local user. Python TLS claiming to be Chrome. Metronome timing claiming to be human. Mid-session IP changes claiming to be one person.

Your job isn't to "beat" the detection — it's to stop contradicting yourself. When every layer of your request tells the same story, there's very little left to score against you. In my production pipelines, getting the story consistent (geo-matched residential IPs, sticky sessions, jittered timing, coherent headers) keeps sustained success rates above 99% — not through clever evasion, but through the absence of anything suspicious to detect.


Full disclosure: I run my data collection through Thordata's residential proxy network — their gateway handles the geo-matching and session stickiness mentioned above (rotating from $0.65/GB, static at $0.75/IP, and code thor020 gets you 10% off if you're setting up your own pipeline). Every technique in this article works with any equivalent provider.

Top comments (0)