By Rajesh Kannan, Co-Founder & CTO, Gramosoft · Last updated: · TLS, header and JavaScript results measured in our own local tests on November 2, 2026; traffic figures from Imperva's 2025 Bad Bot Report and Cloudflare's own posts

Your scraper gets a 403, a CAPTCHA, or a page that looks fine but holds nothing. Somewhere between the TCP connection and the tenth page, the site decided you weren't a person. This guide walks through the 13 signals anti-bot systems check, in the order a visit exposes them, with our own measurements of what Python clients and headless Chrome give away, and how GcrawlAI sends a visitor that holds together.

TL;DR

  • It's a score, not a rule: sites add up many weak signals and act on the total: allow, challenge or block.
  • Most scrapers are caught before the page loads: a datacenter IP and a non-browser TLS fingerprint are read at the connection.
  • Our test: python-requests sent 5 headers to Chromium's 14, and its JA4 fingerprint looked nothing like Chrome's.
  • Headless Chrome tells on itself: navigator.webdriver true, "HeadlessChrome" in the User-Agent, zero plugins and a software GPU.
  • Contradictions weigh most: a Chrome User-Agent on a Python handshake is worse than either alone.
  • Fastest route: GcrawlAI sends a real browser through residential IPs, so all 13 signals agree.

How do websites detect scrapers? They score 13 kinds of signal together: IP reputation and geography, TLS and HTTP/2 fingerprints, headers, User-Agent and timing, JavaScript challenges, automation flags, device fingerprints, behaviour, navigation and cookies, and honeypot traps. No single check blocks you; the combined score and the contradictions between layers do. GcrawlAI keeps every signal consistent in one request.

The 13 signals at a glance

# Signal When it's read What gives a scraper away
1 IP reputation and ASN Connection A cloud or datacenter range, or an IP with a bad history
2 Geography Connection IP country disagrees with timezone or language
3 TLS fingerprint (JA4) Connection A library's handshake, not a browser's
4 HTTP/2 fingerprint Connection SETTINGS values and frame order unlike Chrome, or no HTTP/2
5 Header set and order First request Too few headers, in the wrong order
6 User-Agent and client hints First request A default library UA, or a UA that disagrees with sec-ch-ua
7 Rate and timing First requests Too fast, too evenly spaced, too many at once
8 JavaScript challenge Page load No JavaScript ran, or the sensor never reported
9 Automation flags Page load webdriver true, "HeadlessChrome", missing browser objects
10 Device fingerprint Page load Canvas, WebGL, fonts and screen that don't add up
11 Behaviour Session No pointer, scroll or keyboard rhythm
12 Path and cookies Session Straight to the data, no assets loaded, cookies dropped
13 Honeypots Session Following a link or filling a field no human could see

Send one coherent visitor with GcrawlAI

GcrawlAI answers all 13 checks with the same identity. Each request runs in a real browser through a residential IP in the country you choose, so the IP, TLS handshake, headers, JavaScript view and session tell one consistent story. When a site still challenges, stealth escalates and the request retries with a fresh identity, and failed requests are never billed.

GcrawlAI homepage with the logo and menu (Products, Playground, Extractors, Docs, Pricing, Blog, Use Cases, Custom Request): Transform any website into structured LLM-ready data, the open-source web scraping API for AI agents, RAG pipelines and data teams, with JavaScript rendering, anti-bot stealth and proxies included in every request and no credit system; General, Extractors and Offline Bundle modes, tabs for Scrape, Map, Crawl, Batch, Screenshot, Search and Smart Answer, and the Batch tab open with a URL list, Markdown format, country picker and a Start batch scraping button
The GcrawlAI homepage doubles as a playground: here the Batch tab queues a list of URLs as one job, with rendering, anti-bot stealth and proxies included in every request (desktop screenshot taken October 31, 2026; the phone view, from October 28, shows on small screens).

The homepage puts it plainly: JavaScript rendering, anti-bot stealth and proxies are included in every request, with no credit system. That matters here because the signals in this guide are read together. Fixing your IP but not your handshake, or your headers but not your browser, only moves the contradiction somewhere else.

Two-column diagram: what a protected site checks and what GcrawlAI sends for each. IP reputation and geography: a residential IP in your chosen country. TLS and HTTP/2: a real browser's handshake. Headers, User-Agent and timing: a full header set in browser order, paced. JavaScript, automation flags and device fingerprint: a real browser with stealth escalation. Behaviour, path and cookies: one session that keeps its cookies. Honeypots and challenges: pages rendered like a visitor, with a new identity if challenged. Result: clean Markdown, HTML or JSON, or one error never billed
13 checks, one coherent visitor: what the site checks and what GcrawlAI sends.

Your code asks for a page and gets content back, or one error to queue. A 4xx from the GcrawlAI API itself means a key, plan or payload problem, not something to retry:

# pip install gcrawl-sdk
"""Fetch a protected page through GcrawlAI: every detection layer handled in one request."""
import logging
import os

from gcrawl_sdk import GcrawlClient, ScrapeOptions
from gcrawl_sdk.exceptions import GcrawlAPIError, GcrawlTimeoutError

log = logging.getLogger("scraper")
gc = GcrawlClient(api_key=os.getenv("GCRAWL_API_KEY", "gc-YOUR_API_KEY"))


def fetch_markdown(url: str, geo: str = "US") -> str | None:
    """One request: rendering, residential IPs, stealth escalation and internal retries included."""
    try:
        page = gc.scrape(url=url, options=ScrapeOptions(markdown_enabled=True, proxy_geo=geo), wait=True)
    except GcrawlTimeoutError:
        log.warning("timeout, requeue: %s", url)          # the API call itself timed out
        return None
    except GcrawlAPIError as err:
        if err.status_code < 500:                         # key, plan or payload problem: fix, don't retry
            log.error("request rejected %s: %s", err.status_code, err.response_data)
            raise
        log.warning("api %s, requeue: %s", err.status_code, url)
        return None
    return page.markdown or None                          # empty = nothing useful on the page


if __name__ == "__main__":
    md = fetch_markdown("https://example.com/")
    print(md[:300] if md else "no content, queued for retry")
  • Network signals (1–4): rotating residential IPs with country targeting, and a real browser's TLS and HTTP/2 handshake.
  • Request signals (5–7): the browser's own header set, User-Agent and client hints, with concurrency limits per plan.
  • Page-load signals (8–10): real JavaScript execution and stealth escalation for the automation and fingerprint checks.
  • Session signals (11–13): sessions that keep cookies, pages rendered the way a visitor sees them, and a fresh identity when a challenge appears.
  • Scrape, crawl, screenshots and Search API: scrape to Markdown, HTML or JSON, crawl whole sites, capture screenshots, or use the Search API.
  • For developers and AI agents: a Python SDK, an MCP server for Claude, Cursor and other agents, the Playground to test a page that keeps flagging you, and the docs.

The MIT-licensed core runs on your own servers if you'd rather own the stack:

git clone https://github.com/GramosoftAI/GcrawlAI.git && cd GcrawlAI
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt && playwright install
cp .env.example .env                      # add your database, Redis and proxy settings
python -m api.core.db_setup
redis-server &
celery -A web_crawler.crawler.celery_config worker -l info &
uvicorn api.api:app --port 8000

Pricing is a free plan of 50 requests a month with no card, paid plans from $0.67 to $1.45 per 1,000 requests, and a self-hosted core that costs nothing. The plan table is in the cost section.

Where it falls short: GcrawlAI fetches public pages; it won't log in for you or get past a site that refuses every visitor on a path, and those URLs come back as an error. Every request runs a browser through residential IPs, so plain pages cost the same as hard ones, and the ecosystem is younger, with fewer ready-made integrations.

"Teams ask us which signal got them blocked. Usually it's two signals disagreeing: a Chrome User-Agent on a Python handshake, or a US IP with a German timezone. The detector isn't looking for a bot. It's looking for a story that doesn't add up."

Rajesh Kannan, Co-Founder & CTO at Gramosoft

How detection works: a score, not a rule

Anti-bot systems don't block on one clue. They collect signals from the connection, the request, the page and the session, add them into a risk score, and act on thresholds: let the visit through, show a challenge, or block it. Each signal is weak alone. Signals that contradict each other are the strongest evidence of all.

Timeline infographic of the 13 signals in the order a visit exposes them: before the page (IP reputation, geography, TLS fingerprint, HTTP/2 fingerprint), the request (header set and order, User-Agent and client hints, rate and timing), page load (JavaScript challenge, automation flags, device fingerprint) and the session (behaviour, path and cookies, honeypots), each with what gives a scraper away
The 13 signals, in the order a visit gives them away.

The stakes are high on both sides. Imperva's 2025 Bad Bot Report found automated traffic made up 51% of web traffic in 2024, the first time in a decade it passed human traffic. Most protected sites buy detection from a handful of vendors: Cloudflare, Akamai, DataDome, HUMAN (formerly PerimeterX), Kasada, AWS WAF and Imperva. Their checks differ in depth, but they read the same 13 kinds of signal.

  • Allow: the page loads normally.
  • Challenge: a JavaScript interstitial or a CAPTCHA, which is usually the visible result of signals you tripped earlier.
  • Block: a 403, a 429, or a vendor block page.
  • Soft block: a 200 with empty results or a decoy page, the hardest to notice.

Detection is also moving later in the visit. In July 2026 Cloudflare introduced Precursor, a session-based script that collects pointer movement, keyboard timing, focus changes and page visibility, and keeps its behavioural profile across page refreshes. A clean first request no longer guarantees a clean tenth one.

What our own tests showed

To see what sites actually read, we pointed four clients at a local server on November 2, 2026 and recorded each TLS handshake, header set and JavaScript view: python-requests, httpx, curl_cffi with its Chrome profile, and Chromium 141 driven by Playwright. No site was involved, so nothing here depends on any vendor's rules.

Our measurements from November 2, 2026: JA4 fingerprints of python-requests and httpx (t13d1812h1_85036bcba153) differ from Chromium 141 (t13d1515h2_8daaf6152771), curl_cffi's Chrome profile shares Chromium's cipher hash, and Chromium's JA3 changed between two visits while JA4 did not; and a table of what JavaScript sees in headless versus headed Chromium: webdriver true in both, HeadlessChrome versus Chrome User-Agent, 0 versus 5 plugins, window.chrome undefined versus object, a Notification and permissions mismatch, an unframed window, and a SwiftShader WebGL renderer in both
What we measured: TLS fingerprints by client, and what a page sees in automated Chromium.

Three findings stand out. The Python clients announced themselves before sending a single header: their JA4 fingerprint shares nothing with Chromium's. Chromium's own JA3 hash changed between two visits while its JA4 stayed fixed, which is why detectors moved to JA4. And default headless Chromium showed all seven automation tells we checked; running it headed fixed five of them.

The capture scripts (a raw TLS listener that computes JA3 and JA4, a header echo server and the JavaScript probe below) are in the code download, so you can run them against your own stack.

Before the page loads: signals 1 to 4

The first four signals are read at the connection, before the site renders anything or runs a script. They're cheap for the defender and decisive: an IP from a cloud provider or a handshake from a Python library can end the visit at the edge, and no header or delay you add later is ever read.

1. IP reputation and ASN

Every IP belongs to a network (its ASN), and that network says who you are. Ranges owned by AWS, Google Cloud, Azure or DigitalOcean start with little trust, because people don't browse from server farms. Residential and mobile IPs start neutral. Addresses with a history of abuse, or listed by proxy and VPN databases, start worse. The same scraper that works from your laptop and fails on a cloud server is almost always failing here.

2. Geography

Detectors compare where the IP is with what the browser says: timezone, Accept-Language, locale and sometimes currency. A residential IP in Brazil with a US English browser and a New York timezone is a contradiction a real visitor rarely produces. Pick the proxy country to match the audience, then make the browser agree with it.

3. TLS fingerprint (JA3 and JA4)

Every HTTPS connection opens with a ClientHello listing the client's ciphers, extensions and settings. Hashing it gives a fingerprint of the software, not the person. JA3 hashed the list in order; since Chrome started shuffling its extensions in 2023, the field moved to JA4, which sorts them first. Our capture shows both effects:

Client Ciphers / extensions ALPN JA4
python-requests 2.34 18 / 12 http/1.1 t13d1812h1_85036bcba153_b26ce05bbdd6
httpx 0.28 18 / 12 http/1.1 t13d1812h1_85036bcba153_d41ae481755e
curl_cffi, Chrome 152 profile 15 / 17 h2 t13d1517h2_8daaf6152771_cb7bf5808d99
Chromium 141, two visits 15 / 15 h2 t13d1515h2_8daaf6152771_22334254f9f7 both times

The Python clients offer more ciphers, no HTTP/2 and a completely different cipher hash. curl_cffi's Chrome profile matched Chromium's cipher hash exactly, with a newer Chrome's extension list. Chromium's JA3 was a173af80… on the first visit and 2b928676… on the second, while JA4 didn't move.

4. HTTP/2 fingerprint

A browser that negotiates HTTP/2 sends a SETTINGS frame with its own values, window updates and pseudo-header order. Chrome's pattern differs from Firefox's, and both differ from most HTTP libraries. Our Python clients offered only HTTP/1.1, which on a site every browser reaches over HTTP/2 is a signal in itself. Akamai in particular is known for reading this layer closely.

The request: signals 5 to 7

Once the connection is accepted, the site reads the request itself: which headers arrived, in what order, what the User-Agent claims, and how fast requests keep coming. These are the signals most tutorials cover, and they're the easiest to fix, but fixing them alone does nothing if signals 1 to 4 already flagged you.

5. Header set and order

Browsers send a full, predictable set of headers in a fixed order. Our echo server recorded what each client sent to the same URL:

Client Headers Order (first five)
python-requests 5 Host, User-Agent, Accept-Encoding, Accept, Connection
httpx 5 Host, Accept, Accept-Encoding, Connection, User-Agent
curl_cffi (Chrome profile) 17 sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, upgrade-insecure-requests, user-agent
Chromium 141 (headless) 14 Host, Connection, sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform

No Accept-Language, no sec-fetch-*, no client hints: a five-header request is easy to spot. (curl_cffi's extra headers over plain HTTP include an HTTP/2 upgrade request, which wouldn't appear on HTTPS.)

6. User-Agent and client hints

The default User-Agents were python-requests/2.34.2 and python-httpx/0.28.1: an announcement, not a disguise. Headless Chromium sent HeadlessChrome/141.0.0.0, and its sec-ch-ua listed Chromium without "Google Chrome". Swapping in a Chrome User-Agent string fixes the label but not the handshake, so the request now claims Chrome while its TLS says Python, a stronger signal than the honest default.

7. Request rate and timing

Detectors watch volume per IP, concurrency, and the spacing between requests. A request exactly every 500 ms is mechanical; so are ten parallel connections from one address. Rate limits answer with 429s and often a Retry-After header; ignoring it turns a warning into a block. Pace with jitter, keep one or two requests in flight per site, and back off when told to.

Page load: signals 8 to 10

If the page runs JavaScript, it can inspect the browser that's running it. This is where plain HTTP clients fail outright, because they never run the script, and where automated browsers get caught, because the script can see they're automated. These three signals decide most fights with headless browsers.

8. JavaScript challenges

Anti-bot scripts run in the page, collect a sensor payload and exchange it for a cookie that later requests must carry. A client that never runs JavaScript never gets that cookie, so protected requests fail or return a challenge page such as Cloudflare's "Just a moment…". Some vendors add proof-of-work puzzles or WebAssembly checks that are harder to fake than plain JavaScript.

9. Automation flags

Under automation, navigator.webdriver is true by the WebDriver standard, and headless mode leaves more traces. This probe prints what a page sees:

"""What does an automated Chrome reveal to a page's JavaScript? Prints the values anti-bot scripts commonly read.
pip install playwright && playwright install chromium
Run headless:   python headless_probe.py
Run headed:     python headless_probe.py --headed        (on a server: xvfb-run python headless_probe.py --headed)
"""
import json, sys
from playwright.sync_api import sync_playwright

PROBE = """async () => {
  const gl = document.createElement('canvas').getContext('webgl');
  const dbg = gl && gl.getExtension('WEBGL_debug_renderer_info');
  let perm = 'n/a';
  try { perm = (await navigator.permissions.query({name: 'notifications'})).state; } catch (e) {}
  return {
    webdriver: navigator.webdriver,
    userAgent: navigator.userAgent,
    plugins: navigator.plugins.length,
    languages: navigator.languages.join(','),
    webglRenderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : 'no WebGL',
    windowChrome: typeof window.chrome,
    notification: `${Notification.permission} / permissions API: ${perm}`,
    screen: `${screen.width}x${screen.height}, window outer ${outerWidth}x${outerHeight}`,
  };
}"""

headed = "--headed" in sys.argv
with sync_playwright() as p:
    browser = p.chromium.launch(headless=not headed)
    page = browser.new_page()
    page.set_content("<html><body>probe</body></html>")
    print(json.dumps(page.evaluate(PROBE), indent=2))
    browser.close()

Our results with Chromium 141 on a Linux server:

Property Headless (default) Headed (under Xvfb)
navigator.webdriver true true
User-Agent HeadlessChrome/141 Chrome/141
navigator.plugins 0 5
window.chrome undefined object
Notification vs permissions API denied vs prompt (mismatch) denied vs denied
Window frame outer = inner size outer larger than inner
WebGL renderer SwiftShader (software) SwiftShader (software)

Running headed fixed five tells, but webdriver stayed true and the software GPU still says "server". Patching properties one by one creates its own signal: detectors check whether a property has been overridden, not only its value.

10. Device fingerprint

Scripts draw hidden canvas and WebGL images, read the GPU renderer, list fonts, measure audio output and check screen size and hardware. Faking any one is easy; making them agree is hard. A Windows User-Agent with Linux fonts, or a gaming GPU claimed beside a SwiftShader renderer, is a contradiction. The fix is a real, consistent device profile, not a random one per request.

The session: signals 11 to 13

The last signals build up over seconds or minutes: how the visitor moves, what it requests, whether it keeps its cookies, and whether it touches things no person could see. A scraper can pass every check on the first page and still be flagged on the fiftieth, because these signals score the session, not the request.

11. Behaviour

People move the pointer in curves, scroll in bursts, pause to read and type with uneven gaps. Scripts either produce no events at all or produce them too evenly. Session-level scripts like Cloudflare's Precursor score that rhythm across the whole visit, so behaviour is also why very high page rates from one session look wrong even with perfect fingerprints.

12. Navigation path and cookies

A visitor arrives from somewhere, loads images and stylesheets, and keeps cookies between pages. A scraper often jumps straight to deep URLs, skips every asset, walks pagination in perfect order, and drops cookies between requests, so it never looks like a returning visitor. Anti-bot cookies such as __cf_bm, _abck or datadome are tied to the identity that earned them; reuse them with a different IP and they become evidence.

13. Honeypots

A honeypot is a link or form field hidden from people with CSS, off-screen positioning or aria-hidden. A crawler that follows every anchor in the raw HTML, or fills every input, flags itself at once. This filter drops the obvious traps before your crawler follows or fills them:

"""Drop links and form fields a human can't see before your crawler follows or fills them.

Static HTML only catches inline styles and attributes; trap classes defined in a stylesheet need a real
browser (Playwright's locator.is_visible()). Use this as a first filter, not the only one.
pip install beautifulsoup4
"""
import re
from bs4 import BeautifulSoup

HIDDEN_STYLE = re.compile(
    r"display\s*:\s*none|visibility\s*:\s*hidden|opacity\s*:\s*0(\.0+)?\s*(;|$)"
    r"|(left|top)\s*:\s*-\d{3,}px|(width|height)\s*:\s*0(px)?\s*(;|$)", re.I)


def is_hidden(tag):
    """True if the tag or any ancestor is hidden by attribute or inline style."""
    for node in [tag, *tag.parents]:
        if getattr(node, "attrs", None) is None:
            continue
        if node.has_attr("hidden") or node.get("aria-hidden") == "true":
            return True
        if HIDDEN_STYLE.search(node.get("style", "")):
            return True
    return False


def visible_links(html, base_selector="a[href]"):
    soup = BeautifulSoup(html, "html.parser")
    keep, traps = [], []
    for a in soup.select(base_selector):
        bucket = traps if is_hidden(a) or "nofollow" in (a.get("rel") or []) else keep
        bucket.append(a["href"])
    return keep, traps


def fillable_fields(html):
    """Visible inputs only: never fill a field a person couldn't see."""
    soup = BeautifulSoup(html, "html.parser")
    return [i.get("name") for i in soup.select("input[name], textarea[name]")
            if i.get("type") != "hidden" and not is_hidden(i)]


if __name__ == "__main__":
    page = """
    <a href="/products">Products</a>
    <a href="/trap-1" style="display:none">offers</a>
    <div style="position:absolute; left:-9999px"><a href="/trap-2">archive</a></div>
    <a href="/trap-3" aria-hidden="true" tabindex="-1">.</a>
    <a href="/admin" rel="nofollow">admin</a>
    <form><input name="email"><input name="website" style="visibility:hidden"><input type="hidden" name="csrf"></form>
    """
    keep, traps = visible_links(page)
    print("follow:", keep)
    print("skip:  ", traps)
    print("fill:  ", fillable_fields(page))
    assert keep == ["/products"] and len(traps) == 4 and fillable_fields(page) == ["email"]
    print("All checks passed.")

Static parsing only sees inline styles; traps hidden by a stylesheet need a real browser's visibility check, such as Playwright's is_visible().

Which signal caught you?

Read the symptom before changing anything. Each pattern points to a layer, and fixing the wrong layer wastes a week. Hold everything constant and change one thing at a time: the IP type, then the client, then the browser. Whichever change flips the result is the signal that caught you, and the table shows where GcrawlAI covers it.

Symptom Likely signals Where GcrawlAI fits
Works at home, blocked on a cloud server 1 IP reputation GcrawlAI routes through residential IPs
403 on the first request in Python, fine in Chrome 3–6 TLS, HTTP/2, headers GcrawlAI sends a real browser's handshake and headers
Wrong prices or a consent wall 2 geography GcrawlAI picks the IP country per request
429s after a burst 7 rate and timing GcrawlAI concurrency limits and async crawl jobs
"Just a moment…" or a challenge page 8 JavaScript challenge GcrawlAI renders every page in a real browser
Headless works locally, fails on the real site 9–10 flags and fingerprint GcrawlAI stealth escalation inside the request
Fine for minutes, then blocked 11–12 behaviour and session GcrawlAI retries with a fresh identity
Blocked right after crawling links 13 honeypots Crawl with GcrawlAI and filter hidden links

What staying undetected costs

Every signal you fix yourself costs money or time: residential bandwidth, browser servers, and the engineer who keeps them matched to each site's checks. For 100,000 successful protected pages a month, credit-based scraping APIs cost $249 to $399 and GcrawlAI costs $101; for plain pages that never trigger detection, GcrawlAI is pricier.

Option, 100,000 successful pages a month Plain HTML pages Protected pages (JS rendering + premium proxies) Are failed requests billed?
GcrawlAI (every request rendered, residential IPs included) $101 (Growth $79 + 20,000 overage at $1.10 per 1,000) $101 (same request, no multiplier) No: failed requests are never charged
ScraperAPI $49 (Hobby, 100,000 credits) $299 (Business; 25 credits per page = 2.5M credits) Charges only 200 and 404 responses
ZenRows $39 (Build B3, 120,000 credits) $399 (Growth G3; 25 credits per page = 2.5M credits) Failed requests never charged; 404 and 410 count as successful
ScrapingBee $49 (Freelance, 250,000 credits; JS off) $249 (Business; 25 credits per page = 2.5M credits) Charges only 200, 404 and 410

List-price arithmetic, not a benchmark. Credit costs and billing rules come from ScraperAPI's credit docs, ZenRows' pricing docs and ScrapingBee's FAQ, checked November 1, 2026. The honest read: for plain HTML that never trips detection, ZenRows, ScraperAPI and ScrapingBee cost less than GcrawlAI. GcrawlAI wins on pages behind the signals in this guide, because residential IPs, a real browser and stealth are already in the one request.

GcrawlAI plans (GcrawlAI pricing, checked October 28, 2026):

Plan Price Requests/month Concurrent Per 1,000 Overage per 1,000
Free $0 forever, no card 50 2 – –
Starter $29/month 20,000 10 $1.45 $1.60
Growth (most popular) $79/month 80,000 35 $0.99 $1.10
Pro $199/month 250,000 50 $0.80 $0.90
Scale $499/month 750,000 100 $0.67 $0.75
Enterprise Volume pricing 2M+ Custom – –
Self-hosted $0 (MIT) Your servers Your servers Infrastructure only –

Every plan, Free included, has scrape, crawl, the Search API, JavaScript rendering, residential IP rotation and stealth escalation in each request. Failed requests are never charged. Annual billing charges 10 months for 12, unused requests on paid plans roll over once, and Indian customers are billed in INR with a GST invoice.

When doing it yourself is enough

Many sites run no serious detection at all. For those, a polite client with a full header set, sensible pacing and cookies is enough, and the probes in this guide tell you quickly whether a site reads deeper signals. Bring in GcrawlAI when blocks come back after every fix and keeping signals consistent becomes the job.

Even then you can stay on your own servers: the MIT-licensed GcrawlAI core self-hosts for free, and the free Cloud plan's 50 requests a month are enough to test the pages that keep flagging you. Paste one into the Playground to compare. For the build-or-buy trade-off, read web scraping with Python vs a scraping API.

"Detection is a moving target because the vendors retrain every week. The scrapers that last aren't the cleverest; they're the most boring: one consistent identity, a polite pace, and the humility to stop when a site says no."

Rajesh Kannan, Co-Founder & CTO at Gramosoft

Detection is also a signal to respect

Being detected isn't always a puzzle to solve. A login wall, a terms-of-service ban, personal data, a robots.txt disallow or a refusal that survives a clean session are the site saying no. Looking like a person doesn't give you permission to data behind a control, and the right answer is an API, a licence or a request.

Keep request rates low enough never to burden a site, identify yourself where that's expected, and don't collect personal data you have no basis to hold. For the legal background, read what web scraping is and where it's legal, and get advice for your own case.

Frequently asked questions

How do websites detect scrapers?

They score many signals together: IP reputation, geography, TLS and HTTP/2 fingerprints, headers, User-Agent, timing, JavaScript challenges, automation flags, device fingerprints, behaviour, cookies and honeypots. No single check decides; the combined score and contradictions between layers do.

What is the most common reason scrapers get blocked?

The network layer: a datacenter IP combined with a non-browser TLS fingerprint. Both are read at the connection, before any header or page content, so later fixes never get a chance to count.

Can websites detect headless Chrome?

Yes. In our test, default headless Chromium reported navigator.webdriver true, "HeadlessChrome" in its User-Agent, zero plugins, no window.chrome and a software WebGL renderer. Running it headed fixed some tells, not all.

What is JA4, and how is it different from JA3?

Both hash the TLS ClientHello. JA3 keeps the extension order, so Chrome's shuffled extensions give it a new JA3 on each connection; JA4 sorts them first and stays stable. In our test Chromium's JA3 changed between visits while JA4 didn't.

Does rotating User-Agents stop detection?

No. The TLS handshake, header order and JavaScript checks still identify the client, and a User-Agent that disagrees with them adds a contradiction. Rotation also breaks the consistent identity a session needs.

Do residential proxies stop detection?

They fix one signal: IP reputation. A residential IP with a Python handshake, a headless browser or a mismatched timezone still gets caught. Proxies help only when everything else already looks like a browser.

Is GcrawlAI open source, and can I use it commercially?

Yes. The GcrawlAI core is MIT licensed on GitHub, so you can self-host it and use it commercially. The managed Cloud API adds residential IPs and stealth escalation, with a Python SDK and MCP for AI agents, a free plan of 50 requests a month and paid plans from $0.67 to $1.45 per 1,000 requests.

Send one coherent visitor

Start with 50 free requests a month, no card. Send the pages that keep flagging you through GcrawlAI, get clean content back, and pay only for requests that succeed.

CTA Illustration