Every request tells a story.
I spent 7 years learning to read it.
I started scraping in 2018. Since then I have built hundreds of production spiders across five companies, and spent that time understanding how modern anti-bot systems decide what to trust. This guide is what actually held up in production.
Paste into Claude, ChatGPT or Cursor. The full download already carries the graph ahead of the prose; the two smaller files are for when you only want the structure.
The part most guides
conveniently skip
Bypassing detection is only half the question. The other half is whether you should, whether it is legal where you operate, and whether your approach survives contact with reality. This section comes first on purpose. It is deliberately honest about the limits of everything that follows.
This is not legal advice, but you need to think about these
The case law: who actually won
Round one. On 20 July 2026 the court granted SerpApi's motion to dismiss. Claims resting on search results containing no copyrighted content were thrown out with no leave to refile, on the straightforward reasoning that the DMCA does not protect material that is not copyrighted. The court left a narrow opening: 21 days to amend as to results carrying a copyrighted component, such as the snippets in Knowledge Panels.
Round two. Google refiled on the final day, 10 August 2026, with a 15-page amended complaint built around the single element the court had found missing, written permission from the copyright owners, and naming Reddit as the partner that asked it to block scrapers. The question has therefore moved from whether scraping public pages is lawful to whether the platform holds licences that make the content protectable in the first place. That is a licensing-chain argument, not an access one, and nothing in hiQ or Van Buren answers it.
Why a scraper should care more about this than about the CFAA rulings. If the circumvention theory ever lands, the legal question stops being "were you authorised" and becomes "did you bypass a protection measure", which is a far worse question to be asked, because bypassing is the thing you demonstrably did. The counterweight is that Google has to characterise its own results page as full of protectable copyrighted work, which is the opposite of what it has argued for twenty years about everyone else's content, and any win becomes precedent pointed back at it. Treat this as unsettled; it is the only live case on this page.
Why it belongs beside the case law rather than in a privacy footnote. Google v. SerpApi turns on whether a platform holds licences that make content protectable, which is a question about a chain of permissions. This is the same chain one layer up and considerably murkier: models are trained on brokered data whose provenance and consent basis cannot be inspected from outside, sold by companies not meeting the obligations they already have. If you scrape public pages and keep provenance on what you collected, you are on firmer ground than a good deal of the licensed market. That is genuinely useful to know and not a licence to relax, because enforcement attention that arrives for brokers historically arrives for everyone nearby. Practical version: keep provenance on your rows, know your legal basis for any personal data, and never assume "we bought it" is a stronger position than "we collected it openly".
The line is moving: agents, robots.txt, and the end of a clean binary
robots.txt as the polite fence between them. That split is dissolving, and it changes how you should think about both the ethics and the defences. The scale of the shift is no longer hypothetical: in June 2026 Cloudflare reported that automated traffic had crossed the majority line for the first time in the web's history, at 57.5% of HTTP requests to HTML content against 42.5% from humans, driven mostly by agentic AI. Read the number carefully, it measures crawlable web content rather than every packet, but the direction is unambiguous: the web is now a machine-to-machine environment as much as a human one, which is the whole reason this skill set stopped being niche.There is a second number underneath the first that matters even more for anyone who scrapes. Cloudflare's crawl-to-refer ratio measures how many pages a platform crawls for every visitor it sends back. In 2026 the AI crawlers sit at extraordinary imbalances, with Anthropic's reported in the thousands to tens of thousands of pages crawled per referral and other AI operators in the hundreds to low thousands, against a traditional search engine like Google at roughly five to one. Read these as dated, volatile snapshots, the published figure for a single operator has swung from six figures to low five figures inside a year and moves month to month, so treat any specific ratio as a reading rather than a constant. The structure is what is stable: the dominant readers of the web now consume vastly more than they return, which is exactly why so much of the web is hardening against automated reading at the same time as automated reading becomes the majority of traffic. That tension, more machines reading, more defences raised against them, is the backdrop the rest of this guide operates in.
Start with the uncomfortable truth about
robots.txt: it was never a security control. It is a request, and only the bots that choose to listen ever obeyed it. That was a workable social contract when the only things crawling the web were search engines and the occasional scraper. It breaks the moment an autonomous agent with a set of tools and a goal is involved. Give an agent the instruction to gather something and a browser to do it with, and a blocked default user agent does not stop it, it problem-solves around the block, picks a tool that blends in, and keeps going, without ever being told to evade. The obedience was always voluntary, and agents do not share the assumptions that made it hold.
navigator.webdriver reports false, the same value a human-operated browser returns. The signal that reliably meant "automated" for a decade is being switched off from inside the platform, not defeated from outside. A defender can no longer treat the presence of automation as proof of bad intent, and a scraper can no longer assume that "looking automated" is what gets it blocked.The legislative response: transparency, not technical blocking
robots.txt file to the statute book and the contract. Two 2026 developments mark the direction. In the United States, New York passed a Stealth Crawler Prohibition Act (through the state Senate and Assembly, awaiting the governor's signature at the time of writing), which targets bots that scrape news content while evading detection. It would make it an offence to damage, impair, or burden the operation of a covered news site, let aggrieved publishers subpoena a service provider to identify an alleged violator, and allow them to seek injunctions and damages. In the United Kingdom, a Private Members' Bill, the Automated Online Software (Access and Transparency) Bill, backed by the News Media Association, takes a deliberately narrower line: it does not try to regulate AI models or dictate behaviour, it requires that a bot accessing a site and taking content disclose who it is and what it will do with what it takes. Private Members' Bills rarely become law without government backing, but they shape the bills that follow.
The three tiers of AI access, and where the pressure actually is
The other direction: sites that hand agents a sanctioned door (WebMCP)
The missing half of the handshake: proving who you are
Read this before you copy anything from this guide
The scraping
decision flow
Walk steps in order. Stop at the first win. Complexity and cost increase right. Most production scraping is solved at steps 1–3.
The flow below assumes you have already decided to collect the page yourself. Often that is the wrong assumption, and the argument that follows is opinion against opinion. Below are the five lines worth scoring for both routes — the vendor's official API and scraping the site — and for each one, the metric that settles it and the reason that metric beats the obvious one. The lines are the same everywhere. The answer is different for every source, which is exactly why it is worth measuring rather than debating.
Run both routes down all five, per source, on your own numbers — not a vendor benchmark and not a blog's. Start with coverage, because it is the only line that can return zero and a zero there makes the rest academic. Then read the five together rather than totting up a winner: the first four tell you how last month went, and only the fifth tells you what next year costs.
In production the answer is often both: take the API for the fields it has and scrape the delta. That hands you something you cannot buy — where the two carry the same field and disagree, your parser is wrong until proven otherwise. The API stops being a data source and becomes a correctness oracle.
One thing these five lines deliberately do not score: whether you are allowed to. Terms of service, rate limits, personal data and the relevant case law sit above the scorecard, not inside it — and a route that scores well on all five is still the wrong route if it is one you should not take. Start at Read this first.
_abck, cf_clearance, datadome, reese84), identifies sensor payload endpoints, and tells you which step from the flow below will actually work for this target. What used to be a 4-hour manual walk through HTTP history is now a 2-minute prompt.
Frida · mitmproxy
chompjs · Parsel
Scrapy · Scrapling
CloakBrowser
Zyte · Firecrawl
First, place the target on the difficulty ladder
Sweep the impersonation profile before you escalate the tool
A 2026 write-up of a production escalation put numbers on it. The team held request, headers, cookies, rate and IP conditions constant and changed only the impersonation profile. Against one Cloudflare deployment, the generic Chrome profile returned almost no usable responses while Firefox reached roughly 80%. Behind DataDome on a server-rendered route, chrome120 returned 0 usable responses out of 6, and firefox144 returned usable content in about 7 attempts out of 10. Same library, same everything else, different profile.
Read those as a dated snapshot of two specific deployments rather than a rule about Firefox, because the useful lesson is procedural. A protection provider rejects a connection pattern, not a library, and the pattern that fails today is not the only pattern the library can produce. Before you accept the cost of a browser, sweep the profiles you already have and change one variable at a time. The version of this that saves the most time is written down: keep the profile that worked in your target notes, because the next engineer on that domain will otherwise repeat the whole sweep.
The six steps above tell you what order to try. But to know which step to stop at, and why skipping ahead costs you days, you first need to understand how the detection actually works. Let's go deeper.
Before you send a single byte,
you've already been judged.
The moment your scraper opens a TCP connection to a CDN, a fingerprinting pipeline triggers. By the time your HTTP request body arrives, four independent scoring systems have already assigned you a trust score. Here's exactly what each one measures, and why defeating just one is never enough.
sec-ch-*), and your request timing and rate. That is seven independent signals scored before a single line of your JS runs, which is why a flawless in-page fingerprint cannot save a client whose network-layer story does not match: if the headers announce Chrome 140 while the TLS handshake is Python's OpenSSL, the contradiction is decided before your first document.querySelector. The practical takeaway threads through this whole section, the JavaScript layer is the last thing a server learns about you, not the first, so a scraper that only manages its in-page fingerprint has already lost the connection it never knew it was being judged on.
Layer 1, TLS Fingerprinting: The Handshake That Betrays You
This fires before a single HTTP byte is exchanged. Understanding it is non-negotiable.
TLS Version + Cipher Suites + Extensions + Elliptic Curves + Curve FormatsThis produced a stable 32-char hex hash. Python's
requests library has always had the same JA3 hash. Every major anti-bot catalogued it. By 2021, your Python scraper was identifiable before the first HTTP header.JA3's weakness: Chrome started randomising TLS extension order in 2022. Same browser, different JA3 every session. The fingerprint became unstable and unreliable.
JA4 format:
t13d1516h2_8daaf6152771_b0da82dd1658,
t13 = TLS 1.3, d = DTLS, 1516 = cipher count+length hash, h2 = ALPN (HTTP/2), remainder = extension hashJA4+ extends this with: JA4H (HTTP header fingerprint), JA4X (X.509 certificate), JA4SSH (SSH handshake), JA4T (TCP window + options). Cloudflare deployed it in a Rust crate at CDN edge. Akamai in an EdgeWorker. Both fire before your request reaches origin.
HEADER_TABLE_SIZE, MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, MAX_FRAME_SIZE, MAX_HEADER_LIST_SIZEChrome's exact values are documented. Python's
httpx sends different values. curl sends different values. The ordering of these settings, the window update frame sizes, and the HPACK compression decisions all create a secondary fingerprint that cannot be spoofed without rewriting the HTTP/2 clientwhich is exactly what curl_cffi does.
Chrome's QUIC stack differs from libcurl's QUIC implementation differs from Python's
aioquic. Each leaves a unique signature in the Initial packets.Current status: JA4+ covers QUIC. Cloudflare has begun collecting QUIC fingerprints. Not yet widely enforced for blocking, but the infrastructure is live. Tools like curl_cffi are actively implementing QUIC parity.
# Test your actual JA4 fingerprint against tls.browserleaks.com import requests from curl_cffi import requests as cffi # ❌ requests, exposes Python/urllib3 JA4, blocked immediately r1 = requests.get("https://tls.browserleaks.com/json") print(r1.json()["ja4"]) # → t13d1516h2_8daaf6152771_b0da82dd1658 (Python fingerprint, catalogued, blocked) # ✓ curl_cffi, emits Chrome 124's exact JA4 hash, HTTP/2 frames, cipher order r2 = cffi.get( "https://tls.browserleaks.com/json"– impersonate="chrome124" # also: chrome110, chrome107, safari17 ) print(r2.json()["ja4"]) # → t13d1517h2_c4b4b4b4b4b4_aaaaaaaaaa (Chrome 124 fingerprint, passes) # Also check HTTP/2 fingerprint print(r2.json()["http2"]) # Chrome's exact SETTINGS frame values
All the JA4+ research is academic until you ship it. Three tiers of solution, in order of how often you should reach for each:
curl_cffi (Python), tls-client (Go), noble-tls, hrequests. One line of code, exact Chrome/Firefox JA4. Drop-in replacement for requests.curl_cffi.requests.get(url, impersonate="chrome131")
meta={"stealth": {"profile": "chrome_147"}}
Camoufox, rayobrowse, or CloakBrowser. C++ binary patches ship a real-browser TLS stack along with everything else.Cost: 200MB+ memory per browser instance
urllib3, you flag faster than no spoofing at all, the mismatch is the signal.2. Forgetting HTTP/2 SETTINGS frames. Even perfect JA4 fails if your HTTP/2 SETTINGS (header table size, max concurrent streams, initial window size) do not match the browser you claim to be.
curl_cffi and tls-client handle this; rolling your own usually does not.3. Using stale impersonation profiles. Chrome 120 fingerprints in 2026 are themselves suspicious, real users rolled forward. Keep
impersonate="chrome131" or newer.
requests, Go's crypto/tls for Go clients) with that library's own cipher order, extension set, and ordering, a fingerprint that matches no shipping browser. Tools in the curl-impersonate lineage (and the Go uTLS library that pioneered this) do not tweak that ClientHello, they construct it manually, byte by byte, to reproduce a specific browser's exact handshake. Knowing the tells they have to match is what lets you tell a convincing impersonation from a leaky one.
0x0a0a a client injects so servers stay tolerant of unknown values) is present in Chrome, Edge, and Safari, but Firefox sends none, so a ClientHello that claims to be Chrome with no GREASE is self-contradictory. Safari advertises curves the others usually omit (for example secp521r1) and orders its suites and extensions distinctly. Chrome and Edge are both Chromium but not byte-identical. The renegotiation-info extension (0xff01) is present in stock browsers and a common omission in spoofs. Each TLS library has its own defaults (Chrome uses BoringSSL, Firefox uses NSS), and the gap between those defaults and a browser's exact bytes is exactly what a fingerprint database scores.HelloChrome_131 and the like) because a known-good fingerprint is safer than auto-updating to an untested one, but that stability cuts both ways. Real Chrome rolled forward and randomises its extension order per connection since 2023, so a frozen profile drifts from the live browser, and named uTLS CVEs have shown pinned fingerprints leaking subtle inconsistencies (an ECH cipher choice that real Chrome never makes) on a fraction of connections. Two rules follow. Keep the profile current, a 2024 Chrome hash in 2026 is itself a tell. And keep it coherent across the whole stack: the TLS fingerprint, the HTTP/2 SETTINGS frame, the header wire order, and the User-Agent must all name the same browser, because a Chrome JA3 under a Firefox User-Agent is an instant contradiction.session_ticket extension, and the second reuses the ticket and offers pre_shared_key instead, so the ClientHello bytes, and therefore the JA3 hash, genuinely differ between first and repeat visits. It is not randomness or a timestamp, it is normal protocol behaviour that real browsers and proxies both exhibit, so a differing hash on a repeat test does not mean your spoofing broke. This is also why no serious detector allowlists one exact hash: real browsers already do not produce a single stable fingerprint (Chrome has randomised its TLS extension order since early 2023, which is part of why JA4 sorts extensions before hashing), one vendor reports on the order of fifteen million distinct JA4s across hundreds of millions of real users in a single hour, and a routine client-library update has been documented raising a mobile app's block rate by several percent purely from legitimate users' fingerprints shifting. The takeaway for a scraper is calibrating: the target is to land inside the broad cloud of plausible modern-browser fingerprints, not to clone one canonical hash byte-for-byte, because the detector is scoring closeness among many inputs, not string-matching a single approved value.pre_shared_key (0x0029) and often padding (0x0015), which bumps the extension count and changes the hash, so the same browser on the same machine emits two different JA4s (for example a ...1516... variant with no key and a ...1517... variant with the pre-shared key, note the 16 becoming 17). A proposed refinement, JA4E, strips the ephemeral extensions before hashing to recover one stable per-client identity. From the evasion side this reframes the target: staying coherent now means aligning your TLS state with your application state. If a site has already handed you a session cookie, your HTTP layer is claiming to be a returning visitor, so a cold TLS handshake with no pre_shared_key is a contradiction between the two layers, exactly the kind of cross-signal mismatch the rest of this section is about. A returning-user HTTP state riding a first-time TLS handshake is a tell.signature_algorithms extension, so a fingerprint parser that does not filter GREASE out of that field before hashing will compute a fresh, wrong JA4 on every single request from the same client. If you run a TLS-capture site or a fingerprint database, filter GREASE across every extension, not just the cipher and extension lists, or your own corpus will look like an infinite population of one-off clients. And note the ceiling of the format itself: JA4 (and JA4+) does not natively capture HTTP/2, HTTP/3, or QUIC transport parameters, which is why the HTTP/2 frame-ordering fingerprint (often called the Akamai HTTP2 fingerprint) is tracked separately rather than folded in.Below the handshake: the layer most stealth work never reaches. TLS is where this section starts because it is where most practitioners start, but it is not actually the bottom. The TCP SYN packet that opens the connection carries its own signature: initial window size, maximum segment size, window scaling, the exact set and ordering of TCP options, and the initial time-to-live. Those values come from the operating system's network stack, not from your HTTP library and not from your TLS library, which is precisely what makes them useful to a defender. A request can present a flawless Chrome-on-Windows story at every layer you control and still contradict itself at layer 4, because the packet was assembled by a Linux server kernel. Passive OS fingerprinting from SYN packets is old technique, and it is now being applied to proxy egress specifically: the question stops being what your client claims and becomes what kind of machine actually sent the first packet. It is a small share of blocks compared with IP reputation and behaviour, but it is the residue that survives everything else, and it is invisible from inside the browser.
Layer 2, JavaScript Fingerprinting: The Page That Interrogates You
navigator.webdriver = false for AI-agent-driven Playwright sessions, and Google patched out the most common CDP-detection technique in V8. The browser vendors that wrote the automation-transparency rules quietly stopped enforcing them. Any bypass or detection strategy pivoting on these flags should treat them as unreliable. See the Innovation Feed card "The Vendors That Wrote the Detection Rules" for details.
Once your TLS passes, the page loads its anti-bot script. This is a 500KB+ obfuscated interrogation that runs dozens of tests in parallel.
canvas.getContext('2d') then calls canvas.toDataURL(). The exact pixel output varies by:, GPU manufacturer and model (NVIDIA vs AMD vs Intel)
, Driver version and sub-pixel rendering
, OS-level font rendering (Windows ClearType vs macOS CoreText)
, Canvas size and DPI scaling
A headless Chromium with no GPU produces a software-rendered canvas with a known hash. Botaaurus and CloakBrowser spoof this at the C++ level by injecting slight noise into the pixel values before
toDataURL() returns, enough to vary the hash while remaining visually identical.
gl.getParameter(gl.RENDERER) and gl.getParameter(gl.VENDOR). Real Chrome returns something like ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0).Headless Chrome returns a generic string or crashes on WebGL entirely. Anti-bots cross-reference: if WebGL says "Intel UHD 620" but Canvas hash shows software rendering, that's a contradiction, you're flagged.
WebGL extensions list is also fingerprinted. Real GPUs expose 30–40 extensions. Software renderers expose a different subset. The exact combination is GPU-specific and stable across sessions.
AudioContextgenerates a sine wave through an OscillatorNoderuns it through a DynamicsCompressorNodeand reads the output buffer values. The floating-point output depends on:, CPU architecture (x86 vs ARM floating-point precision)
, Operating system audio stack
, Audio driver implementation
Headless environments often return
0.0 across the buffer (no audio context), or a software-emulated value that differs from hardware. CloakBrowser patches this at the Chromium C++ audio rendering layer.
When JS patches a native function, for example,
navigator.webdriverit replaces the getter with a custom function. Calling Function.prototype.toString.call(getter) on the patched function returns function () { [custom code] } instead of function () { [native code] }.Kasada specifically tests dozens of native functions this way. playwright-stealth patches them in JavaScript, so toString() reveals the patch. PatchRight fixes this at the Python source levelbefore Chrome even starts. There's no JS to inspect.
fetch('chrome-extension://[id]/manifest.json'). Real Chrome browsers have at least a few extensions installed (ad blockers, password managers, etc.).A headless browser returns
net::ERR_FAILED on all 60 requests simultaneously, a statistically impossible result for a real user. The extension IDs probed include:cjpalhdlnbpafiamejdnhcphjbkeiagm (uBlock Origin)hdokiejnpimakedhajhdlcegeplioahd (LastPass)nngceckbapebfimnlniiiahkandclblb (Bitwarden)Fix: CloakBrowser loads real extension profiles. You install 1Password or Bitwarden into it so some probes return real manifest data.
navigator.webdriverCDP-controlled browsers expose themselves through subtler signals:Timing: CDP's
Runtime.enable command leaves a timing gap between page parse and script execution that doesn't exist in real Chrome.Execution context:
window.cdc_adoQpoasnfa76pfcZLmcfl_Array and similar artifacts left by ChromeDriver are checked.Permission API: Real Chrome returns realistic permission states. ChromeDriver returns defaults inconsistent with a "normal" browser.
Plugins: Headless Chrome has zero plugins. Real Chrome always has at least the PDF viewer plugin.
Camoufox's solution: Uses Mozilla's Juggler protocol, which sits below CDP entirely, none of these artifacts exist.
curl_cffi) gets a 200 carrying the sec-cpt challenge; an automation stack (stock Node or Puppeteer Chromium) gets an immediate 403 and never sees the challenge. The trap is what happens next. Even if a real Chromium driven by a stealth tool solves sec-cpt and serialises a mathematically perfect roughly 2.2KB behavioural payload to validate the _abck cookie, the edge still refuses it when the TLS fingerprint on that same connection reads as the automation tool. Perfect behaviour over the wrong TLS escalates to a permanent 403 for the session. The lesson the guide repeats applies at its sharpest here: every layer must agree at once, a flawless payload over a mismatched handshake is worse than useless.window (overriding navigator.webdriver, wrapping DOM methods in a Proxy, replacing toString). Akamai sidesteps all of it by injecting a hidden zero-size iframe and pulling pristine API references straight from iframe.contentWindow, so it can call the original native functions even when the main window is heavily spoofed. It pairs this with Function.prototype.toString checks for the [native code] marker and Object.getOwnPropertyDescriptor inspection of getters that should not exist, then ships an integrity hash to a separate anti-hook endpoint before the main telemetry is even sent. Practical consequence: patching the visible window is not enough, your patches must survive being compared against a clean realm the page can conjure at will.visitorId, and the property that makes fingerprinting matter at all is easy to demonstrate with it: open its demo, then reopen in incognito or after clearing browser data, and the identifier is unchanged, because none of the signals it reads live in cookies or local storage. Running it against your own scraper is the fastest honest audit of how identifiable your setup is, and it pairs naturally with the local BotD and CreepJS harness described just below.tanh and coherence sections, viewed from the vendor's side.$cdc_ markers, which is exactly what BotD keys on. Only undetected-chromedriver, which patches those markers out of the binary, passed outright.
toString that does not read as [native code], a descriptor that should not exist. A consistency-checking detector counts each of those as evidence of tampering. So a half-measure that patches the visible surface while leaving the binary tell in place can score worse than no stealth at all, because you have removed nothing that mattered and added new lies to catch.evaluate(..., return_by_value=True) can silently drop falsy return values if the client's unwrap logic tests truthiness (an if result.value: style check), so a script that legitimately returns false, 0, or "" comes back as nothing. This is quietly catastrophic for stealth work, because the values a bot detector traffics in are exactly the falsy ones: navigator.webdriver is supposed to read false on a good setup, and if false silently vanishes you cannot tell a passing config from a broken read. The clean fix is to stop trusting the transport's unwrap and force a round-trip: wrap the probe in JSON.stringify(...) on the page and JSON.parse (or json.loads) the returned string, so 0, false, "", and nested objects all survive verbatim (the one edge is a script evaluating to undefined, which stringifies to a bare word rather than valid JSON, so treat a non-string result as an unusable signal). The general lesson beyond this one library: when you read detection signals through any automation transport, verify the transport does not reshape your values before you trust a single result, because a corrupted read looks exactly like a passing check.HeadlessChrome user agent or anything that reads as CDP automation. Everything that still leaves navigator.webdriver true sat at the bottom, and the middle tier was, in the author's phrase, one user-agent string away from a clean pass. That is the same rebuild-beats-patch conclusion from the matrix above, reached with a different tool set, which is exactly the kind of agreement that tells you the ranking measured something real.channel="chrome") is what makes the comparison measure stealth rather than which Chromium happened to ship. The general habit both of these teach is to calibrate against outside referees rather than your own probe, cross-checking against independent bot-detection pages that return a structured verdict keeps your weighting honest.navigator.cpuPerformance returns a stable integer tier from 1 (low) to 4 (high), with 0 for unknown, a coarse hardware class handed over with no benchmark, no opt-out, and no cross-origin limit. There is also a managed-Chrome policy, CpuPerformanceTierOverride, that pins the reported tier — which is worth knowing in both directions: a supported way to set the value without patching anything, and a reason a detector cannot treat the tier as ground truth. It is a WICG proposal, deliberately bucketed (each tier must cover a few hundred CPU models) to cap the entropy it adds, but Mozilla still estimates it at one to three extra bits per user and WebKit declined to implement it, which tells you how the other engines weigh the tradeoff. There is even a Chrome Enterprise policy to override the value (0 to 4).navigator.hardwareConcurrency of 16 (spoofed) on a 2-vCPU cloud box whose cpuPerformance lands at a low tier, and the two claims disagree loudly. Push further and a smart WASM micro-benchmark (SIMD timings, feature detection) fingerprints the actual silicon and either corroborates the tier or exposes the lie. The practical lesson for a scraper is the one this section keeps arriving at: if you spoof one hardware property you now own all of them, the tier, the core count, and the measured compute all have to tell a single coherent story, and a headless fleet on undersized cloud instances is exactly where they stop doing so.maxTouchPoints to a mobile value, shipping a real Widevine CDM. But the deep fixes are architectural and cannot be faked from JavaScript: to make the SIMD CPU oracle agree with the ARM claim you have to actually run on ARM64 hardware or a VM, and a JS override of PaymentRequest or the GPU renderer leaves its own tamper artifact that a coherence check then catches. That is the whole thesis of this section in one page: the shallow tells are a checklist you can grind through, but the ones that decide the verdict require the environment to genuinely be what it claims, which is why a desktop box pretending to be a phone loses no matter how many properties it patches.sin, cos, or tanh to be correctly rounded to the last bit. Every operating system ships its own math library (glibc on Linux, libsystem_m on macOS, UCRT on Windows) with its own polynomial coefficients and rounding, so the same call returns bits that differ in the last place depending on the OS underneath.Math, V8 bundles its own implementation, statically linked and identical on every OS, so sin, cos, and pow give the same bits everywhere and leak nothing. Math.tanh is the exception. Since Chrome 148, V8 stopped computing tanh with its own bundled routine and now calls std::tanh, which reads the host OS math library. The result: on Chrome 148 and later, Math.tanh of the right input returns Linux bits on Linux and Mac bits on Mac, and a Linux server spoofing macOS is caught the instant a page evaluates it, because the browser genuinely cannot change the answer, the OS produced it. Chrome 147 and earlier do not leak here, which itself pins a version range.Math is a tell in essentially one place, but CSS trig functions leak everywhere, because the rendering engine calls the host math library directly for every sin, cos, tan, and inverse. A defender can probe those and also the domain edges, where implementations diverge more loudly: asin(2) is out of domain and resolves to zero on a real Mac (the NaN is clamped), not the ninety degrees a naive reproduction returns. The lesson for a scraper is the sharpest version of this whole section's theme. You cannot patch your way to a coherent lie here, because the signal is produced below the browser, by hardware and the OS, so the only setup that survives a math probe is one whose claimed OS is the OS it runs on. Spoofing the User-Agent is free; making the silicon agree is not.fdlibm port with LLVM's libc, and each migration moves bits: Chrome 149 and 150 disagree on roughly 4.5% of sampled inputs to the migrated functions, every disagreement being the old implementation off by one unit in the last place. The change is permanent in a specific way, correct rounding has only one answer, so once an engine gets there the wrongness that made it distinctive is gone, and every engine that arrives spits out identical bits. The wrongness was the signal. The practical fallout is that a fingerprint library holding a table of "what Chrome returns" goes stale silently: run a current CreepJS-style table against a freshly migrated Chrome and a large share of the math rows no longer match its Chrome column, and some now match Firefox instead. The trig rows went stale back in Chrome 110 for the same reason. The takeaway for both sides: the useful key is not "Chrome" but the exact build, chrome_149 and chrome_150 are different fingerprints, and treating an engine family as one stable value is how a detector starts misclassifying its own real users, and how a spoofer targets a value the real browser stopped returning two versions ago.Math.pow and Math.tanh never used V8's math library at all (pow sits behind a use_std_math_pow flag defaulting true; tanh is one line calling std::tanh), so both go straight to the operating system's libm, ucrtbase on Windows, glibc on Linux. That is why Math.pow(17, 13) returns 9904578032905936 on Windows and 9904578032905938 on Linux, same browser, same version, and why a table that files tanh(0.123) under a single "Chrome" value gets it wrong on one of the two OSes. The vivid case: because that call now hits the system libm, Linux Chrome returns a value a stale table labels as Chrome while Windows Chrome returns one the same table labels as Safari, identical browser and version, two different engine labels, purely from the OS underneath. It is the cleanest possible proof that these values measure the platform, not the browser string, and that any fingerprint keyed on the browser name is standing on sand.accept-language, the sec-fetch-* set, sec-ch-ua, and so on. On some detectors (DataDome is the reported case) that is exactly backwards. Those headers exist because a browser is rendering a page and running JavaScript. Send the full bundle from a client that plainly is not executing any JS, with no telemetry, no canvas, no timing to justify them, and the mismatch is the tell: the detector flags the combination of browser-grade request metadata with a non-browser client, not any single header. A documented case had a controlled one-variable-at-a-time matrix (proxy, JA3 profile, header order, cache-control all changed in isolation) return a 403 every time, until the header set itself was cut back to a lean, honest handful (user-agent, priority, cache-control), at which point the same endpoint returned 200 and real HTML, no browser and no JS involved. The principle generalises the coherence theme one layer up from TLS and hardware: more apparent realism is not more human when the signals you add have nothing behind them. A client that honestly sends almost nothing is more coherent, and often less suspicious, than one wearing a full browser costume it cannot back up, so match your header set to what your client can actually justify rather than to the longest bundle you can copy.:method :authority :scheme :path; most HTTP libraries emit a different order entirely (a common one is :method :path :scheme :authority), and that mismatch is exactly why a request with otherwise flawless headers still dies against Akamai. The trap is that you often cannot fix it from your own code: the library reorders your headers underneath you, and on the classic Python stack urllib3 reorders again below that, so setting the headers in a dictionary does not help because something downstream reshuffles them before they hit the wire. The reliable fixes are the ones that own the whole emission path: use curl_cffi, which impersonates Chrome's full profile with header and pseudo-header order included, or drive a real browser. Right values in the wrong order is not a disguise, it is a tell, and it is one of the most common reasons a hand-built HTTP client that looks perfect on paper fails the moment it meets a serious anti-bot.How an extension list gets built, and why it reads as evidence of a person. The anti-bot vendors that probe for browser extensions are exploiting a gap the browser deliberately left. Chrome exposes no API that reports which extensions are installed, and that is an intentional anti-fingerprinting decision. Individual extensions can still be detected one at a time, and enough of those checks in sequence reconstructs a list.
The main technique is resource probing. An extension declares web_accessible_resources, files it will serve to any page that asks, so a script can request a known file path under chrome-extension:// with the extension's fixed identifier and learn from success or failure whether it is present. Run that against a catalogue of popular extensions and you have an enumerated list.
The signal is unusually good for a defender on two counts. It carries real entropy, because the combination of a dozen installed extensions is close to unique, and it is stable, because people rarely add or remove them, so the same list returns tomorrow. The second count is the one that decides tool choice: an extension list is decent positive evidence of a human, since automated browsers almost never ship with any. That is the reasoning behind the vendor probe covered in the Akamai profile, and it is why the browsers that clear it do so by loading genuine password-manager profiles rather than by patching a response. An empty extension surface is not neutral. It is a statement.
Layer 2.6, Side Channels And State Leaks: The Signals Nobody Thinks To Patch
The main technique is resource probing. An extension declares
web_accessible_resources, files it will serve to any page that asks, so a script can request a known file path under chrome-extension:// with the extension's fixed identifier and learn from success or failure whether it is present. Run that against a catalogue of popular extensions and you have an enumerated list.The signal is unusually good for a defender on two counts. It carries real entropy, because the combination of a dozen installed extensions is close to unique, and it is stable, because people rarely add or remove them, so the same list returns tomorrow. The second count is the one that decides tool choice: an extension list is decent positive evidence of a human, since automated browsers almost never ship with any. That is the reasoning behind the vendor probe covered in the Akamai profile, and it is why the browsers that clear it do so by loading genuine password-manager profiles rather than by patching a response. An empty extension surface is not neutral. It is a statement.
navigator.storage is backed by disk, in a private window it is backed by RAM, and writing to RAM is measurably faster than writing to disk. Write and flush a single byte, time it, repeat a few times to weed out noise, and a flush under roughly a tenth of a millisecond gives away a private session.The lesson is broader than incognito: a detector does not need to read a property you spoofed, it can measure a physical consequence of your environment that no fingerprint patch touches. Timing is not on most people's spoofing checklist.
The takeaway for anyone building scrapers: stealth is not a setting you flip on. Any API behaviour, any implementation quirk, any undefined edge case can become a fingerprint. You cannot enumerate them in advance, which is why coherence across the whole environment matters more than patching individual tells.
The fix is to stop isolating at the profile level and isolate at the process level: one identity, one process, and ideally one host with a coherent fingerprint, rather than many personas multiplexed through a single long-lived browser. It costs more to run. It is also the only isolation the deeper signals respect.
The two properties that make this matter for a scraper: it needs zero permissions, browsers do not prompt for disk access, and neither an ad blocker nor a private window mitigates it. Until vendors fuzz or round these timings there is no clean client-side fix. The general lesson repeats: hardware leaves a signature your fingerprint patch never touches, and a farm of identical VMs can look too identical at the disk layer.
sec-ch-ua header looks like noise: "Chromium";v="149", "Not A Brand";v="24". It is not random, it is a pure function of the Chromium major version. The brand ordering, the greasey "Not A Brand" label, and the version permutation are all seeded from the major version number over fixed lookup tables, no rand(), no timestamp. Every machine on the same Chromium version produces an identical header.That determinism is a trap if you spoof carelessly. Hand-roll a
sec-ch-ua that does not match the algorithm for the Chrome version you are claiming, and you have manufactured a contradiction a detector can check with one lookup. If you set the version, derive the header from it rather than copying one from a different build.
You cannot enumerate them all in advance. The defensible posture is not to chase each new tell, it is to keep the whole environment internally consistent and let real hardware speak for itself wherever you can, rather than presenting a hand-assembled identity that has to be right on every one of a thousand axes at once.
Layer 2.5, WebAssembly Fingerprinting: The Layer Below Your Stealth Browser
The probe: set
hyphens: auto on a narrow container, render a known word like "hyphenation", read the rendered width or screenshot via Canvas. A real Chrome on Windows produces hy-phen-ation. A custom fork without the dictionary produces no break, or the wrong break.Affected stealth browsers: anything built from a custom Chromium source that skipped the hyphenation step, which is most of them. Real CloakBrowser and properly-built forks include it, hand-rolled patches usually don't.
Mitigation: confirm your build ships the dictionary for every language you claim to support, or run a real Chrome binary under XVFB. Verify with the live PoC: joe12387.github.io/hyphenation-dictionary-poc · github source
Why this matters: stealth browsers like Camoufox, CloakBrowser, PatchRight patch what the browser reports. WASM SIMD probes the actual CPU. A real Mac with M2 chip can't be spoofed to look like an Intel laptop because the SIMD timing fingerprint is generated by the silicon, not by the browser.
Source: Anthony Manikhouth (DataDome bot detection engineer), blog.azerpas.com, May 2026.
performance.now() at 100µs on non-isolated pages to prevent Spectre-style timing attacks. But one line of JavaScript breaks that: new WebAssembly.Memory({shared:true}).buffer returns a real SharedArrayBuffer on any page, no special headers required.
Paired with a
MessageChannel ping-pong loop in a hidden iframe driving Atomics.add(), you get a counter incrementing at 100,000 Hz, distinguishing steps around 6µs. That's 17× finer than the timer Chrome intends you to have.
Why anti-bots love this: micro-timing patterns (canvas render time, JS jitter, animation frame variance) differ between humans and bots at sub-millisecond scale. WASM shared memory makes that measurable on every page, not just cross-origin-isolated ones. Reported to Chrome as crbug 40057687, marked Won't Fix.
Source: Manuel (brokenbrowser.com).
1. Anti-bot ships a WASM module with SIMD ops and a high-resolution timer built from
WebAssembly.Memory({shared:true}).2. The module runs natively, no JS hooks to intercept, no
Function.toString() traces to leak.3. CPU microarchitecture + timing patterns are POSTed back as part of the bot scoring payload, often alongside the canvas hash.
What this defeats: Camoufox (Firefox C++ patches), CloakBrowser (49 Chromium patches), PatchRight, undetected-chromedriver, Nodriver, Pydoll. All of them patch JS APIs and binary internals, but none patches the WASM execution layer.
What still works: real hardware diversity. Different physical machines produce different SIMD fingerprints naturally. The future of stealth scraping is less about better lies and more about real hardware in real consumer locations, which is exactly what residential proxies on real ISP IPs already approximate.
indexedDB.databases(), the names come back in hash table iteration order, which is deterministic and stable for the lifetime of that process. Two unrelated sites see the same ordering and can use it to silently link a user's activity across domains — no cookies, no shared storage, no user interaction required.The fingerprint persisted across reloads, new private windows, and even Tor Browser's "New Identity" resets. Only a full browser restart cleared it. Fixed in Firefox 150 / Tor Browser 15.0.10 (April 21, 2026).
Scraper implication: If you run multiple scraping identities inside the same browser process (shared Camoufox instance, same Firefox PID), an anti-bot can correlate them using this ordering as a stable session token — regardless of proxy rotation, cookie isolation, or fingerprint patching. The signal is below every stealth layer.
Rule: Isolate scraping identities at the process level, not just the profile level. One identity = one browser process. Verify your Camoufox build is on Firefox 150+.
Ref: CVE-2026-6770 · mfsa2026-30 · SecurityAffairs writeup
Layer 3, Network Identity: The Five Vectors That Must Agree
geoip=True aligns WebRTC candidates with the proxy exit country.IP country, timezone, Accept-Language, WebRTC candidate, DNS resolver location. A US proxy with Accept-Language: ur-PK fails immediately. All five must tell a consistent geographic story. This is why setting geoip=True in Camoufox is critical, it auto-configures all five to match the proxy's exit country.Layer 3.5, DOM Honeypots: The Trap Doesn't Care About Your Fingerprint
display:none, visibility:hidden, opacity:0zero-dimension elements, off-screen positioning, fields with tabindex="-1"or links placed after the closing </body> tag.getBoundingClientRect()) before interacting.Layer 4, Behavioural ML: You Can't Fake Being Human
document.querySelector() after DOMContentLoaded looks nothing like a human who reads the page for 2.3 seconds first. Warm-up navigation (visiting homepage before target) significantly improves behavioural scores.2026 update, the session becomes the unit of judgement. In July 2026 Cloudflare shipped Precursor, which productises exactly the principle this layer describes. Rather than scoring a request, it injects JavaScript dynamically and collects behavioural signals continuously as a visitor moves through an application, feeding them into bot scoring in real time across the whole session.
The reasoning Cloudflare gives for building it is worth reading carefully, because it is a concession as much as a capability. Modern automation can execute JavaScript, drive a genuine browser environment, and pass an individual challenge. What it has not learned to do is stay convincing over time. So the defence stops asking whether this request looks legitimate and starts asking whether this journey does, which is the same move Akamai made with multi-request trust accumulation and the same reason a warm-up navigation improves your score.
The practical consequence for anyone building automation is that per-request thinking is now the wrong altitude. A stateless design that treats every fetch as independent has no way to be coherent over a session, because there is no session to be coherent within. This is also why a stateless per-request browser engine, however efficient, is the wrong tool against a target scoring at this layer.
Layer 5, Fingerprint Replay: The Game Stopped Being About Spoofing
The reasoning Cloudflare gives for building it is worth reading carefully, because it is a concession as much as a capability. Modern automation can execute JavaScript, drive a genuine browser environment, and pass an individual challenge. What it has not learned to do is stay convincing over time. So the defence stops asking whether this request looks legitimate and starts asking whether this journey does, which is the same move Akamai made with multi-request trust accumulation and the same reason a warm-up navigation improves your score.
The practical consequence for anyone building automation is that per-request thinking is now the wrong altitude. A stateless design that treats every fetch as independent has no way to be coherent over a session, because there is no session to be coherent within. This is also why a stateless per-request browser engine, however efficient, is the wrong tool against a target scoring at this layer.
Real fingerprints trade as operational assets. I have seen forum threads pricing them around five dollars per thousand, and one ecosystem scan found roughly one in eight bot-adjacent sites running a fingerprinting collector. The implication for you: a perfectly coherent fingerprint is not necessarily a spoofed one. It may be a real environment, lifted from a real person, replayed at scale.
The harvested collectors I have looked at do not stop at raw attributes. They mirror vendor-specific challenge logic: Akamai-style feature bitmasks, PerimeterX canvas markers, payment-provider canvas seeds, hashed feature keys aligned with named CAPTCHA vendors. The dataset is dual layer: raw device signals plus vendor-shaped outputs. That is what makes replay viable against a specific wall rather than just "looking human."
The takeaway I keep coming back to: coherence and freshness now matter more than any one spoofed attribute. A clean canvas hash from last week, replayed today against a vendor that rotated its logic, fails harder than no spoofing at all. Treat your fingerprint as a living thing that has to agree with itself across every context, every request.
Now you know the detection layers, four signal families plus the replay economy sitting on top of them. Every vendor below is just a different weighting of those same signals, some prioritise TLS, others behaviour, others network identity. Knowing the layer tells you which tool to pick. Here are the six walls.
Six companies built the walls.
Here's every key.
Each vendor applies the detection layers differently, different weights, different signals, different architectures. What bypasses Cloudflare has zero effect on Kasada. You need to know exactly which wall you're facing before you choose a tool.
Identify which anti-bot you're facing
Wrong strategy on the wrong vendor wastes hours. Before writing a single line of code, spend 30 seconds identifying exactly what's protecting the target.
Visit the target site, click the Wappalyzer icon in your toolbar. It instantly shows all detected technologies, including the anti-bot vendor. Shows Akamai, Cloudflare, DataDome, PerimeterX, Kasada and more with a single click.
Open DevTools → Application → Cookies. Match any cookie name to identify the vendor. Multiple vendors can run on the same site. For CLI scanning at scale: wafw00f https://target.com identifies WAF + anti-bot vendor in one command.
DevTools → Network → any request → Response Headers. Look for x-datadome, server: cloudflare, x-akamai-request-idor challenge redirect URLs containing vendor names.
Free Chrome + Firefox extension. One click on any site shows:
- Anti-bot / security vendor
- CDN provider
- CMS, framework, analytics
- Server technology
curl_cffi impersonate="chrome124" handles TLS + HTTP/2 layergeoip=True, 100% pass rate Mar 2026 on Instagram, Reddit, X, LinkedInStealthyFetcher solves Turnstile natively and automaticallyboring_challenge is a Rust-compiled state machine that cannot be emulatedit requires actual browser execution to produce valid tokens. IP reputation alone accounts for 25–30% of the total trust score.__NEXT_DATA__ in HTML source, Grainger had 110KB of product data in it, bypassing DataDome entirelycurl_cffi chrome124 + residential proxy → confirmed 200 OK (Grainger.com)geoip=Truealigns all 5 identity vectors with proxy exit country_px3 token generation flow for token replayips.jsrenamed polymorphically each deployment) issues proof-of-work challenges that require real CPU cycles and browser APIs to solve. There are no CAPTCHAs, failures are silent 403s or 429s with no explanation. The critical 2026 fact: Kasada specifically fingerprints playwright-stealth by calling Function.prototype.toString() on patched native functions. The patch signatures are catalogued.robots.txt. Used by Codeberg, FFmpeg, the Linux kernel source, Sourcehut, and most non-Cloudflare FOSS projects. Recognisable by its anime "Anubis" mascot illustration during the challenge. Bypass: headless Chromium with JS enabled (it'll solve the PoW naturally, just slower), or persist the verification cookie across requests. Codeberg confirmed in mid-2025 that AI scrapers already learned to solve Anubis challenges, so it slows scraping but doesn't stop a determined operator._fs_ch_st_* marks a challenge starting and _fs_ch_cp_* marks it solved, with _fs_cd_cp_* appearing when advanced client-side detection is enabled. A solved challenge yields a token cookie (default one hour) that later requests must carry. Because the challenge is a JS PoW rather than a heavy obfuscated VM, a real JS-executing browser engine clears it where a plain HTTP client cannot, which puts it closer to Anubis in difficulty than to Kasada. The usual caveat holds: identify it from the _fs_ch_* cookies first, then match a real browser stack end to end (TLS, header order, and JS execution) rather than reaching for a heavier tool than the challenge needs.Quick identification reference
| What you see | Anti-bot | Key cookie/header | Detection method |
|---|---|---|---|
| "Pardon Our Interruption" page | Akamai block | _abck | Wappalyzer · response body |
| CF-Ray header · Turnstile iframe | Cloudflare challenge | cf_clearance | Response header CF-Ray |
JSON with datadome key | DataDome block | datadome | Response header x-datadome |
_px3 or _pxde set | PerimeterX block | _px3 | Cookie inspection |
| Silent 403 · no body | Kasada silent | x-kpsdk-ct | Response headers · ips.js in source |
reese84 or TS cookie | F5 Shape block | reese84 | Cookie names · Shape JS reference |
| Anime mascot "weighing your soul" page | Anubis challenge | techaro.lol-anubis-auth | JS PoW challenge · Anubis HTML title |
| 302 redirect to a virtual waiting room | Queue-It queue | Queue-it token cookie | X-Queueit-Connector header · queue-it.net redirect |
Six walls. Now the tools. Every library below exists as a direct response to one of those six systems, curl_cffi was built because JA4 broke Python's TLS. Camoufox because CDP leaks signal automation. PatchRight because Kasada fingerprints JS patches. The arms race made this arsenal.
How I approached real-world bypasses
The theory above tells you what anti-bots do. These notes tell you what I did when I hit them on a production job. Each is a full day or two of work distilled to: what I tried, why it failed, what finally worked, and the decision tree I'd use next time.
Akamai v3 in 2026: cracking it without a browser
Field notes from a production scraping job. The story of what I tried, why each thing failed, and the exact approach that finally got clean 200 responses with zero browser overhead.
_abck ~-1~ won't flipAkamai's _abck cookie has two states. ~-1~ means unvalidated, full bot score, blocked. ~0~ means validated, trust granted. The cookie is set immediately on any page load, but only flips to ~0~ after sensor.js (a 512KB obfuscated fingerprinting script) executes, collects signals, and POSTs them to /_bm/data.
Signals that matter most: canvas fingerprint (pixel-level hash of GPU-rendered shapes and text), WebGL renderer (exact GPU model via WEBGL_debug_renderer_info), AudioContext (floating-point sine wave through a compressor node), Chrome extension probes (60 chrome-extension:// URLs fetched via fetch(), zero passing = instant bot score), mouse/scroll trajectory physics, and navigator properties cross-checked against the fingerprint.
The kicker: validation is multi-request. Trust accumulates across the session, not just on the first hit.
undetected-chromedriver (uc) routed through a Comcast ISP proxy. Then switched to Pydoll — CDP automation without the usual webdriver flags. Both behaved identically. _abck set immediately as ~-1~, never flips. Waited 60 seconds, scrolled, dispatched JS mouse events. Nothing.gl.getContext('webgl') returns null. Sensor.js sees zero WebGL context and assigns maximum bot score before the session even starts.--use-angle=swiftshader --use-gl=angle. WebGL works. Canvas renders. AudioContext works. Renderer: ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE))).0x0000C0DE is SwiftShader's device ID, in public lists of virtual GPU IDs. Akamai checks the unmasked renderer against a blocklist. SwiftShader is on it. The canvas hash it produces is also deterministic and known.geoip=True aligns WebRTC, DNS, and timezone with the proxy exit country. Set up a session, pointed it at the target, ran a few warm-up requests.Page.addScriptToEvaluateOnNewDocument: patch WebGLRenderingContext.prototype.getParameter to return "Intel Iris OpenGL Engine". Patch navigator.platform to "MacIntel", deviceMemory to 8, battery API, chrome.runtime. Result: 2327-byte error page before sensor.js runs.Page.addScriptToEvaluateOnNewDocument. The prototype tampering itself is detectable via Function.prototype.toString().Network.setUserAgentOverride with full userAgentMetadata to spoof macOS Chrome 148. No JS injection. Same error page.navigator.userAgent returns, but not the TLS ClientHello fingerprint. Akamai's EdgeWorker sees the JA4 hash (still Linux Python automation) and blocks at the network layer before the page loads.xvfb-run -a, Chrome launches in headless=False. Pages load fully (1.1MB real HTML, all images, category navigation).glxinfo shows Mesa software rasterizer. Canvas hash from Mesa llvmpipe is different from SwiftShader but still a known server software renderer, also flagged. _abck stays ~-1~ for 60+ seconds regardless of scrolling.Page.addScriptToEvaluateOnNewDocument again. Same problem as Attempt 3.Function.toString() inspection.Every failed attempt above tried to fix the browser layer. The fundamental insight: most Akamai-protected sites never reach the deep sensor.js evaluation if the request looks like real Chrome at the network layer first.
Akamai scores in five layers:
Python's requests, httpx, even curl_cffi with a wrong impersonation profile all fail at Layer 1. The JA4 hash doesn't match Chrome 148's actual ClientHello. Fix Layers 1-3 correctly and you often never reach Layer 4.
A Go library, akamai-v3-sensor, reimplements Chrome's exact TLS stack at the C level: cipher suite order, GREASE values, extension ordering, ALPN, HTTP/2 SETTINGS frames, HTTP/3 QUIC parameters. The JA4 fingerprint it produces is indistinguishable from real Chrome 148 because it is Chrome 148's cipher suite, implemented in Go.
// One session, one proxy, one request s := sensor.NewSession("chrome-148", sensor.WithSessionProxy("http://user:pass@comcast-ip:port"), sensor.WithSessionTimeout(30*time.Second), ) resp, _ := s.Get(context.Background(), "https://target-site.com/") // Status: 200, Protocol: h2, _abck: ~0~ (validated) // Then GraphQL directly on the same session gql, _ := s.DoWithBody(ctx, req, bytes.NewReader(payload)) // Status: 200, 30KB product data, zero blocks
No browser process. No GPU. No canvas hash. No sensor.js execution. Just a TLS handshake that matches Chrome 148 exactly because it uses Chrome 148's cipher suites.
Scrapy spider
→ GoProxyMiddleware (urllib, ~35ms round trip)
→ Go HTTP server :8765 (4-session pool)
→ Go TLS library sessions
→ ISP proxy (Comcast AS7015, static residential)
→ Target site
Session rotation logic: 206 or GenericError triggers the next session in the pool. Three errors on one session triggers a background re-warm (new TLS handshake, new session state). All 4 sessions blocked returns 503; Python middleware waits 5s and retries up to 3× before falling back to curl_cffi.
toDataURL() or getParameter() in JavaScript is detectable via Function.prototype.toString(). The only real fix is at the C++ level, either a real GPU or a library that bypasses the browser entirely.
0x0000C0DE device ID is permanently flagged.
Don't bother. It's in Akamai's blocklist and the deterministic canvas hash is also known. Same for Mesa llvmpipe.
Page.addScriptToEvaluateOnNewDocument is itself a signal.
Akamai's EdgeWorker detects the timing gap left by CDP's Runtime.enable command. The injection runs, but the metadata around it is visible.
/graphql, /api/v1/, mobile traffic intercepted via HTTP Toolkit.
Every tool built to fight
every wall we just described.
Now that you understand the detection stack and the six anti-bot vendors, every library below makes sense in context. curl_cffi exists because of JA4. Camoufox exists because of CDP leaks. PatchRight exists because of Kasada's toString() inspection. The arsenal wasn't built randomly, each tool is a direct countermeasure to a specific detection innovation.
curl_cffi both answer it. JavaScript-layer detection reads navigator properties after the page loads, where a current unpatched Chromium already passes most panels. The third surface is the one that quietly defeats expensive tooling: automation-protocol fingerprinting, which detects how the browser is being driven rather than what it claims to be.This is the layer that does not care how good your fingerprint patches are. Anything driving Chrome through Playwright leaves a recognisable shape in the control protocol at startup, the
Runtime.enable and Target.setAutoAttach handshake sequence. A fork can rewrite navigator properties all day without touching it. The tools that clear this layer are the ones that remove the standard automation framework from the control plane entirely: nodriver drives system Chrome over a direct CDP connection with no Playwright shim, which is why it walks through Cloudflare Turnstile gates that every patched Playwright fork fails. The practical lesson from running these side by side: identify the gate's layer first, then pick the cheapest tool that covers it. A twenty-line curl_cffi wrapper can match a 130MB patched Chromium fork on a TLS-and-JS target, and lose entirely on an automation-protocol target where only a non-Playwright control plane gets through. Patches are not the lever you think they are, the control plane is.
Scraping is no longer Python-only.
Python still dominates the open-source ecosystem (Scrapy, curl_cffi, Camoufox), but the hardest 10% of targets in 2026 reach for Go, TypeScript, or Rust. Here's when each language earns its place, and why mixing them in one pipeline is the production-grade move.
urllib for the protected requests only.Master comparison table, all 86 libraries & tools
| Library (click to expand) | Type | Lang | JS render | TLS spoof | TLS detail | Anti-bot target | MCP | Stars |
|---|---|---|---|---|---|---|---|---|
| curl_cffi ⚡ | HTTP | Python | Chrome JA4+ | Akamai, DataDome | – | |||
|
⚡ HTTP
Under the hood: libcurl C library with custom TLS patches. Emits exact Chrome/Safari/Firefox TLS ClientHello at the C level, cipher suites, extensions, ALPN, GREASE all match real browsers.
✓ Pros
✗ Cons
|
||||||||
| Scrapling ⚡ | HTTP | Python | Chrome TLS | Cloudflare Turnstile | 38k | |||
|
⚡ HTTP
Under the hood: Wraps curl_cffi for stealth HTTP + integrates Camoufox for browser mode. StealthyFetcher uses a real patched Firefox under the hood when needed.
✓ Pros
✗ Cons
|
||||||||
| webclaw ⚡ | HTTP | Rust | Chrome TLS | Medium targets | – | |||
|
⚡ HTTP
Under the hood: Rust HTTP client with TLS fingerprint spoofing. Emits browser TLS signatures from Rust, fast and low-memory.
✓ Pros
✗ Cons
|
||||||||
| httpx ⚡ | HTTP | Python | None | Unprotected only | – | |||
|
⚡ HTTP
Under the hood: Modern Python HTTP library with async support and HTTP/2.
✓ Pros
✗ Cons
|
||||||||
| requests ⚡ | HTTP | Python | None | Unprotected only | 52k | |||
|
⚡ HTTP
Under the hood: Pure Python HTTP library. Sends HTTP/1.1 requests with standard Python TLS.
✓ Pros
✗ Cons
|
||||||||
| tls-client ⚡ | HTTP | Go/Py | Chrome/Firefox TLS | Akamai, DataDome | – | |||
|
⚡ HTTP
Under the hood: Go/Python wrapper around a Go TLS client that mimics browser fingerprints. Predecessor to cycle-tls.
✓ Pros
✗ Cons
|
||||||||
| Playwright 🌐 | Browser | Py/JS | CDP (detectable) | Medium (CDP leaks) | 68k | |||
|
🌐 Browser
Under the hood: Chromium DevTools Protocol (CDP). Microsoft-maintained. Drives real Chromium, Firefox, or WebKit browsers over CDP socket.
✓ Pros
✗ Cons
|
||||||||
| Camoufox 🌐 | Browser | Python | C++ Firefox Juggler | Cloudflare 100%, Akamai | – | |||
|
🌐 Browser
Under the hood: Forked Firefox with C++ binary patches to Juggler protocol (below CDP). Patches navigator, canvas, WebGL, fonts, window.chrome at binary level.
✓ Pros
✗ Cons
⚠ CVE-2026-6770 — Process-Level Fingerprint Leak (patched Firefox 150)
Firefox below v150 returned IndexedDB database names in hash table iteration order rather than sorted order. Because the hash table is shared across all origins within the same browser process, the ordering became a stable, high-entropy process-lifetime fingerprint — consistent across tabs, sites, private windows, and even Tor Browser's "New Identity" resets. Anti-bot systems could use this to correlate multiple scraping identities running in the same browser process, regardless of proxy rotation or profile switching.
Fix: Use Camoufox built on Firefox 150+ (patched April 2026). Verify your version. Scraper lesson: Always isolate identities at the process level, not just the profile level. Multiple sessions in one browser process can be correlated through memory-state artifacts like this even when fingerprint patching is otherwise perfect. Ref: CVE-2026-6770 · mfsa2026-30 · Fixed Firefox 150 / Tor Browser 15.0.10 |
||||||||
| CloakBrowser 🌐 | Browser | Python | 49 C++ patches | Akamai, reCAPTCHA v3 0.9 | – | |||
|
🌐 Browser
Under the hood: 49+ C++ binary patches to Chromium itself. Patches webdriver, chrome object, plugins, permissions, WebGL, Canvas, AudioContext, and extension probe responses at the C++ level, not JavaScript. Repo: github.com/CloakHQ/CloakBrowser.
✓ Pros
✗ Cons
|
||||||||
| PatchRight 🌐 | Browser | Python | Py source patches | Kasada, Cloudflare | – | |||
|
🌐 Browser
Under the hood: Patches Playwright Python source files at install time. Removes CDP signatures, webdriver property, and stealth tells from the JS layer.
✓ Pros
✗ Cons
|
||||||||
| Puppeteer 🌐 | Browser | Node | CDP (detectable) | Medium targets | 89k | |||
|
🌐 Browser
Under the hood: Node.js CDP driver for Chromium. Google-maintained. The original headless browser automation library.
✓ Pros
✗ Cons
|
||||||||
| Selenium 🌐 | Browser | Multi | webdriver=true | Weak (legacy) | 29k | |||
|
🌐 Browser
Under the hood: WebDriver protocol (W3C standard). Drives any browser via standardised JSON protocol. The original browser automation framework.
✓ Pros
✗ Cons
|
||||||||
| SeleniumBase UC 🌐 | Browser | Python | UC removes WD flag | Kasada, general stealth | 10k | |||
|
🌐 Browser
Under the hood: SeleniumBase with undetected-chromedriver mode. Patches Chrome binary to remove webdriver flag and CDP signatures.
✓ Pros
✗ Cons
|
||||||||
| Selenium-Driverless 🌐 | Browser | Python | CDP no WebDriver | Medium targets | – | |||
|
🌐 Browser
Under the hood: Direct CDP connection without ChromeDriver binary, no webdriver flag set. Async Python API.
✓ Pros
✗ Cons
|
||||||||
| nodriver 🌐 | Browser | Python | Raw CDP async | Medium targets | – | |||
|
🌐 Browser
Under the hood: Controls Chrome via its internal DevTools socket without using CDP's standard automation flag. Chrome doesn't know it's being driven.
✓ Pros
✗ Cons
|
||||||||
| pydoll 🌐 | Browser | Python | Async CDP | Medium targets | – | |||
|
🌐 Browser
Under the hood: Pure Python browser automation using Chrome DevTools Protocol directly. No external driver.
✓ Pros
✗ Cons
|
||||||||
| Botright 🌐 | Browser | Python | CAPTCHA solving | CAPTCHA targets | – | |||
|
🌐 Browser
Under the hood: Playwright wrapper focused on CAPTCHA solving and stealth. Uses AI to solve CAPTCHAs during automation.
✓ Pros
✗ Cons
|
||||||||
| Botasaurus 🌐 | Browser | Python | Gaussian mouse | DataDome behaviour | – | |||
|
🌐 Browser
Under the hood: Playwright wrapper that adds Gaussian mouse movement, realistic typing, scroll physics, and session management.
✓ Pros
✗ Cons
|
||||||||
| rayobrowse 🌐 | Browser | Py/Docker | Real device FP DB | Hard targets | – | |||
|
🌐 Browser
Under the hood: Docker-based stealth Chromium browser from Rayobyte. C++ level patches (not JS-level), exposed via CDP so Playwright/Puppeteer/Selenium can connect natively. Self-hosted = free and unlimited; managed Cloud version available.
✓ Pros
✗ Cons
|
||||||||
| undetected-chromedriver 🌐 | Browser | Python | Removes WD flag | Medium targets | 5k | |||
|
🌐 Browser
Under the hood: Patches ChromeDriver binary to remove webdriver=true and CDP automation flags at binary level.
✓ Pros
✗ Cons
|
||||||||
| ⭐ Scrapy ⚡ | Framework | Python | Via curl_cffi mw | Medium (with middleware) | 52k | |||
|
⚡ HTTP
Under the hood: Twisted-based async Python framework. Pure HTTP, sends requests, receives responses, parses with XPath/CSS. No browser.
✓ Pros
✗ Cons
|
||||||||
| Crawlee 🌐 | Framework | Node/Py | Playwright-based | Medium targets | 15k | |||
|
🌐 Browser
Under the hood: Apify's unified Node.js framework. Wraps both HTTP (got-scraping) and Playwright/Puppeteer. Handles retries, deduplication, storage.
✓ Pros
✗ Cons
|
||||||||
| scrapy-camoufox ⚡ | Framework | Python | Camoufox integration | Hard targets | – | |||
|
⚡ HTTP
Under the hood: Scrapy middleware that routes requests through Camoufox browser for stealth. Best of Scrapy + Camoufox.
✓ Pros
✗ Cons
|
||||||||
| scrapy-nodriver ⚡ | Framework | Python | nodriver integration | Medium targets | – | |||
|
⚡ HTTP
Under the hood: Scrapy middleware using nodriver for browser requests, Chrome without CDP flags.
✓ Pros
✗ Cons
|
||||||||
| scrapy-stealth ⚡ | Framework | Python | Browser TLS + HTTP/2 | Cloudflare, Akamai | v0.4 (2026) | |||
|
⚡ HTTP
Under the hood: Pluggable Scrapy DOWNLOADER_MIDDLEWARE with three drivers:
basic + turbo (TLS fingerprint + HTTP/2 impersonation, no browser), and browser (real Chrome via CDP for JS-heavy targets). Per-request engine switching via request.meta["stealth"]. Repo: github.com/fawadss1/scrapy-stealth. Author Fawad ships frequent updates.✓ Pros
✗ Cons
|
||||||||
| Firecrawl ⚡ | AI | API | FIRE-1 engine | Hard via managed | 111k | |||
|
⚡ HTTP
Under the hood: API service that converts any URL to clean Markdown or structured JSON for LLM consumption. FIRE-1 agent for multi-page crawls.
✓ Pros
✗ Cons
|
||||||||
| Crawl4AI 🌐 | AI | Python | Playwright-based | Medium targets | 60k | |||
|
🌐 Browser
Under the hood: Local Playwright wrapper optimised for LLM output. Runs locally, converts pages to clean Markdown with BM25 relevance filtering.
✓ Pros
✗ Cons
|
||||||||
| ScrapeGraphAI ⚡ | AI | Python | NL graph pipeline | Light protection | 18k | |||
|
⚡ HTTP
Under the hood: LLM-powered extraction that builds a graph pipeline from a natural language prompt. Local or API.
✓ Pros
✗ Cons
|
||||||||
| Jina Reader API ⚡ | AI | API | Built-in rendering | Medium targets | – | |||
|
⚡ HTTP
Under the hood: REST API: prefix r.jina.ai/ to any URL to get clean Markdown back. Zero setup.
✓ Pros
✗ Cons
|
||||||||
| Steel 🌐 | AI | API | Docker browser | Medium targets | – | |||
|
🌐 Browser
Under the hood: Self-hosted browser API with MCP server. AI agents call it as a tool to browse the web.
✓ Pros
✗ Cons
|
||||||||
| Bright Data ⚡ | Managed | API | Full enterprise stack | All incl. F5 Shape | – | |||
|
⚡ HTTP
Under the hood: 72M+ IP network + scraping API. Managed infrastructure handles anti-bot, JS rendering, proxy rotation.
✓ Pros
✗ Cons
|
||||||||
| Zyte ⚡ | Managed | API | Full stack | All targets | – | |||
|
⚡ HTTP
Under the hood: Scrapy company's managed scraping platform. Zyte API + AutoExtract for structured data.
✓ Pros
✗ Cons
|
||||||||
| Apify ⚡ | Managed | API | 10K+ Actors | Medium-hard | – | |||
|
⚡ HTTP
Under the hood: 10,000+ pre-built Actors on serverless cloud. Crawlee at core. MCP server for AI agents.
✓ Pros
✗ Cons
|
||||||||
| ScrapingBee ⚡ | Managed | API | Managed rendering | Medium targets | – | |||
|
⚡ HTTP
Under the hood: Managed scraping API. Handles JS rendering, CAPTCHA, proxies via simple REST call.
✓ Pros
✗ Cons
|
||||||||
| SerpAPI ⚡ | Managed | API | SERP JSON API | Search engine data | – | |||
|
⚡ HTTP
Under the hood: Managed API that abstracts Google, Bing, Baidu, Yandex, Yahoo, DuckDuckGo and 80+ other engines behind a single REST endpoint. Returns fully parsed, normalised JSON — organic results, ads, featured snippets, knowledge graphs, local packs, shopping, images, news — without you touching a proxy or a headless browser.
✓ Pros
✗ Cons
|
||||||||
| ScrapeBadger ⚡ | Managed | API | Smart billing + AI extract | Cloudflare, DataDome, hard targets | – | |||
|
⚡ HTTP
Under the hood: Newer managed scraping API built natively for modern anti-bot stacks. Key differentiator: smart billing — if you enable JS rendering and anti-bot bypass but the target doesn't need them, ScrapeBadger auto-downgrades the request and charges you less. Also ships an MCP server for Twitter/X scraping (profiles, tweets, trends) for AI agent workflows.
✓ Pros
✗ Cons
|
||||||||
| Oxylabs ⚡ | Managed | API | OxyCopilot AI | Hard targets | – | |||
|
⚡ HTTP
Under the hood: 102M+ IP network with OxyCopilot AI extraction and scraper APIs.
✓ Pros
✗ Cons
|
||||||||
| Browserbase 🌐 | Managed | API | Managed browser | Hard targets | – | |||
|
🌐 Browser
Under the hood: Managed Playwright cloud. Run Playwright scripts remotely without managing browser infrastructure.
✓ Pros
✗ Cons
|
||||||||
| chompjs ⚡ | Parser | Python | N/A | Parser only | – | |||
|
⚡ HTTP
Under the hood: Python library to parse JavaScript objects embedded in HTML pages. Converts JS literals to Python dicts.
✓ Pros
✗ Cons
|
||||||||
| Parsel ⚡ | Parser | Python | N/A | Parser only | – | |||
|
⚡ HTTP
Under the hood: Scrapy's HTML/XML parser library. XPath and CSS selectors with a clean Python API.
✓ Pros
✗ Cons
|
||||||||
| BeautifulSoup4 ⚡ | Parser | Python | N/A | Parser only | 10k | |||
|
⚡ HTTP
Under the hood: Python HTML/XML parser. Wraps lxml or html.parser. Builds a parse tree from raw HTML strings.
✓ Pros
✗ Cons
|
||||||||
| mitmproxy ⚡ | RE Tool | Python | N/A | RE / intercept | 37k | |||
|
⚡ HTTP
Under the hood: Python-based HTTPS proxy. Intercepts, inspects, and modifies HTTP/HTTPS traffic between client and server.
✓ Pros
✗ Cons
|
||||||||
| HTTPToolkit ⚡ | RE Tool | Any | N/A | Mobile API intercept | – | |||
|
⚡ HTTP
Under the hood: HTTPS intercepting proxy for development and mobile API discovery. Open source.
✓ Pros
✗ Cons
|
||||||||
| Frida ⚡ | RE Tool | Py/JS | N/A | SSL hooks | – | |||
|
⚡ HTTP
Under the hood: Dynamic instrumentation toolkit. Injects JavaScript into running processes. Used to hook native functions and bypass SSL pinning.
✓ Pros
✗ Cons
|
||||||||
| rebrowser-patches 🌐 | Browser | Python | Chrome source patches | Medium targets | – | |||
|
🌐 Browser
Under the hood: JavaScript patches injected into Playwright/Puppeteer pages to mask automation signals.
✓ Pros
✗ Cons
|
||||||||
| cycle-tls ⚡ | HTTP | Go/JS | Chrome/Firefox TLS | Akamai, DataDome | – | |||
|
⚡ HTTP
Under the hood: Node.js/Go TLS client that cycles through browser fingerprints. Sends real JA3 hashes per request.
✓ Pros
✗ Cons
|
||||||||
| GoLogin 🌐 | Browser | Cloud | Antidetect profiles | Hard multi-account | – | |||
|
🌐 Browser
Under the hood: Cloud anti-detect browser. Manages browser profiles with unique fingerprints stored in cloud. Multi-account management.
✓ Pros
✗ Cons
|
||||||||
| Multilogin 🌐 | Browser | Cloud | Antidetect profiles | Hard multi-account | – | |||
|
🌐 Browser
Under the hood: Commercial anti-detect browser with managed profile fingerprints. Team collaboration on browser profiles.
✓ Pros
✗ Cons
|
||||||||
| ScraperAPI ⚡ | Managed | API | Full stack | All incl. Walmart | – | |||
|
⚡ HTTP
Under the hood: Simple proxy rotation + JS rendering API. Handles geo-targeting and header rotation.
✓ Pros
✗ Cons
|
||||||||
| Decodo ⚡ | Managed | API | Full stack | All targets | – | |||
|
⚡ HTTP
Under the hood: Smartproxy's new brand. Residential, datacenter, and mobile proxy network.
✓ Pros
✗ Cons
|
||||||||
| CapSolver ⚡ | CAPTCHA | API | N/A | reCAPTCHA/hCaptcha | – | |||
|
⚡ HTTP
Under the hood: AI-powered CAPTCHA solving service. Uses computer vision to solve reCAPTCHA v2/v3, hCAPTCHA, Cloudflare Turnstile.
✓ Pros
✗ Cons
|
||||||||
| 2captcha ⚡ | CAPTCHA | API | N/A | All CAPTCHA types | – | |||
|
⚡ HTTP
Under the hood: Human + AI hybrid CAPTCHA solving service. One of the oldest in the market.
✓ Pros
✗ Cons
|
||||||||
| Anti-Captcha ⚡ | CAPTCHA | API | N/A | reCAPTCHA/image | – | |||
|
⚡ HTTP
Under the hood: Human + AI CAPTCHA solving service. Competitor to 2captcha.
✓ Pros
✗ Cons
|
||||||||
| Scrapyd ⚡ | Framework | Python | Via middleware | Scrapy deploy tool | – | |||
|
⚡ HTTP
Under the hood: Daemon that deploys and runs Scrapy spiders via JSON API. Port 6800. Process-based job queue.
✓ Pros
✗ Cons
|
||||||||
| scrapy-redis ⚡ | Framework | Python | N/A | Distributed Scrapy | – | |||
|
⚡ HTTP
Under the hood: Scrapy extension connecting spiders to a Redis shared URL queue. Enables distributed crawling.
✓ Pros
✗ Cons
|
||||||||
| scrapy-cluster ⚡ | Framework | Python | N/A | Enterprise Scrapy | – | |||
|
⚡ HTTP
Under the hood: Distributed Scrapy cluster using Redis + Kafka + Zookeeper. Enterprise-scale distributed crawling.
✓ Pros
✗ Cons
|
||||||||
| scrapy-poet ⚡ | Framework | Python | N/A | Page Object pattern | – | |||
|
⚡ HTTP
Under the hood: Dependency injection framework for Scrapy spiders. Cleaner spider code with page objects.
✓ Pros
✗ Cons
|
||||||||
| Splash 🌐 | Browser | Docker | Lua scripting | Light protection | – | |||
|
🌐 Browser
Under the hood: Lua-scriptable browser for JS rendering, runs in Docker. Integrates with Scrapy via scrapy-splash.
✓ Pros
✗ Cons
|
||||||||
| selectolax ⚡ | Parser | Python | N/A | Fast HTML parser | – | |||
|
⚡ HTTP
Under the hood: C-based HTML parser (lexbor engine). 10–100× faster than BeautifulSoup for pure parsing tasks.
✓ Pros
✗ Cons
|
||||||||
| lxml ⚡ | Parser | Python | N/A | XPath + CSS parser | – | |||
|
⚡ HTTP
Under the hood: C-based XML/HTML parser. Fastest Python HTML parsing option.
✓ Pros
✗ Cons
|
||||||||
| w3lib ⚡ | Parser | Python | N/A | URL/text utils | – | |||
|
⚡ HTTP
Under the hood: Web-related utility functions. URL normalisation, encoding handling. Used internally by Scrapy.
✓ Pros
✗ Cons
|
||||||||
| SwiftShadow ⚡ | Proxy | Python | N/A | Proxy pool manager | – | |||
|
⚡ HTTP
Under the hood: Free proxy pool manager. Fetches, validates and rotates free proxies automatically.
✓ Pros
✗ Cons
|
||||||||
| requests-ip-rotator ⚡ | Proxy | Python | N/A | AWS API Gateway IPs | – | |||
|
⚡ HTTP
Under the hood: Rotates requests through AWS API Gateway endpoints to get rotating IPs.
✓ Pros
✗ Cons
|
||||||||
| Colly ⚡ | Framework | Go | Go TLS | Medium targets | 15k | |||
|
⚡ HTTP
Under the hood: Go HTTP scraping framework. Fast, concurrent, clean API.
✓ Pros
✗ Cons
|
||||||||
| Katana ⚡ | Framework | Go | Go TLS + Chromium | Medium targets | 8k | |||
|
⚡ HTTP
Under the hood: Go-based web crawler by ProjectDiscovery. Designed for security research and recon.
✓ Pros
✗ Cons
|
||||||||
| playwright-go 🌐 | Browser | Go | CDP (detectable) | Medium targets | – | |||
|
🌐 Browser
Under the hood: Go bindings for Playwright. Same Playwright API in Go.
✓ Pros
✗ Cons
|
||||||||
| Charles Proxy ⚡ | RE Tool | Any | N/A | Mobile API intercept | – | |||
|
⚡ HTTP
Under the hood: Commercial HTTPS proxy for request inspection and debugging. GUI-based.
✓ Pros
✗ Cons
|
||||||||
| Selenoid ⚡ | HTTP | Go (Docker) | Browser-as-a-service | Medium targets | 2.6k | |||
|
⚡ HTTP
Under the hood: Docker containers running headless Chrome/Firefox in parallel, Aerokube's Go-based Selenium grid replacement.
✓ Pros
✗ Cons
|
||||||||
| noble-tls ⚡ | HTTP | Python | Chrome JA3/JA4 | Cloudflare, DataDome | – | |||
|
⚡ HTTP
Under the hood: Python port of uTLS via custom TLS handshake stack, emits browser-matching ClientHello.
✓ Pros
✗ Cons
|
||||||||
| hrequests ⚡ | HTTP | Python | Browser-grade TLS | DataDome, Cloudflare | 900 | |||
|
⚡ HTTP
Under the hood: Drop-in requests replacement with TLS impersonation, header order matching, and optional Playwright browser mode.
✓ Pros
✗ Cons
|
||||||||
| crawlee-python 🌐 | Browser | Python | Via curl_cffi backend | Most targets | 6.2k | |||
|
🌐 Browser
Under the hood: Python port of Apify Crawlee, wraps curl_cffi for HTTP and Playwright for browser modes in a unified framework.
✓ Pros
✗ Cons
|
||||||||
| estela ⚡ | Framework | Python (K8s) | Spider-dependent | Distributed Scrapy | 90 | |||
|
⚡ HTTP
Under the hood: Kubernetes orchestrator for Scrapy, schedules and runs spiders as K8s jobs with auto-scaling.
✓ Pros
✗ Cons
|
||||||||
| fake-useragent ⚡ | HTTP | Python | UA strings only | Lightweight only | 3.8k | |||
|
⚡ HTTP
Under the hood: Curated database of real-world User-Agent strings, sampled from browser telemetry sources.
✓ Pros
✗ Cons
|
||||||||
| grequests ⚡ | HTTP | Python | requests + gevent | Unprotected APIs | 4.4k | |||
|
⚡ HTTP
Under the hood: gevent-monkey-patched requests, fires hundreds of HTTP calls in parallel via greenlets.
✓ Pros
✗ Cons
|
||||||||
| Scrapoxy ⚡ | Framework | Node.js | Proxy manager | Self-hosted rotation | 2.1k | |||
|
⚡ HTTP
Under the hood: Self-hosted proxy pool manager, provisions proxies on AWS, Azure, GCP and rotates IPs automatically.
✓ Pros
✗ Cons
|
||||||||
| primp ⚡ | HTTP | Python (Rust) | Chrome JA3/JA4/HTTP2 | Akamai, DataDome | – | |||
|
⚡ HTTP
Under the hood: Python Requests IMPersonate. Rust binding over a patched reqwest stack, emitting browser-matching TLS and HTTP/2 fingerprints.
✓ Pros
✗ Cons
|
||||||||
| impit ⚡ | HTTP | Rust / Py / JS | Browser TLS + HTTP/3 | Cloudflare, DataDome | – | |||
|
⚡ HTTP
Under the hood: Rust library built on rustls and reqwest, patched to reproduce browser TLS fingerprints without touching system libraries. Successor to got-scraping.
✓ Pros
✗ Cons
|
||||||||
| rnet ⚡ | HTTP | Python (Rust) | Browser JA3/JA4 | Cloudflare, Akamai | – | |||
|
⚡ HTTP
Under the hood: Async Python HTTP client over a Rust TLS stack that emits browser-shaped ClientHello and HTTP/2 settings.
✓ Pros
✗ Cons
|
||||||||
| zendriver 🌐 | Browser | Python | Real Chrome stack | Cloudflare, DataDome | – | |||
|
🌐 Browser
Under the hood: Maintained CDP-driven wrapper around real Chrome with the undetected configuration handled for you, in the nodriver lineage. No Selenium or WebDriver binary.
✓ Pros
✗ Cons
|
||||||||
| scrapy-impersonate ⚡ | Framework | Python | Chrome JA3/JA4 | Akamai, Cloudflare | – | |||
|
⚡ HTTP
Under the hood: Scrapy download handler backed by curl_cffi, so spiders get browser TLS and header order without leaving the Scrapy request cycle.
✓ Pros
✗ Cons
|
||||||||
| Trafilatura ⚡ | Parser | Python | Content extraction | – | ||||
|
⚡ HTTP
Under the hood: Heuristic main-content extractor: strips boilerplate and returns article text plus metadata (title, author, date, language, site name) as text, markdown, or XML.
✓ Pros
✗ Cons
|
||||||||
| Resiliparse ⚡ | Parser | Python / C++ | Content extraction | – | ||||
|
⚡ HTTP
Under the hood: High-performance HTML parsing and main-content extraction from the ChatNoir stack, built for web-scale corpora.
✓ Pros
✗ Cons
|
||||||||
| spider ⚡ | Framework | Rust | High-volume crawling | – | ||||
|
⚡ HTTP
Under the hood: Async Rust crawler that discovers, fetches, and queues at very high concurrency, with a subscribe API for streaming pages as they land.
✓ Pros
✗ Cons
|
||||||||
| browser-use 🌐 | AI | Python | Real browser stack | Interaction-heavy flows | 100k | |||
|
🌐 Browser
Under the hood: Hands a real browser to an LLM agent that clicks, scrolls, types and reads screenshots with vision when the DOM is unusable.
✓ Pros
✗ Cons
|
||||||||
| FlareSolverr 🌐 | Managed | TypeScript | Real browser stack | Cloudflare IUAM | – | |||
|
🌐 Browser
Under the hood: Runs a browser behind a simple HTTP API, solves the challenge, and returns the rendered response, so the browser lives outside your crawler.
✓ Pros
✗ Cons
|
||||||||
| DeepScrape 🌐 | AI | TypeScript | Fingerprint hygiene | Content extraction | 272 | |||
|
🌐 Browser
Under the hood: Self-hosted scraping API: markdown and structured extraction, self-healing CSS specs, an autonomous agent, and an MCP server exposing each saved site spec as its own tool.
✓ Pros
✗ Cons
|
||||||||
Browser engines, deep dive
Runtime.enable timing, execution context leaks, and binding exposure all signal automation. Camoufox uses Mozilla's Juggler protocol below CDP, no CDP leaks. playwright-stealth patches JS at runtime but Function.toString() exposes the patch.pip install playwright && playwright installfrom camoufox.sync_api import Firefoxpip install patchrightpuppeteer-stealth plugin patches common detection points. CDP signature still visible at protocol level. Better for rendering tasks than hard anti-bot targets.navigator.webdriver=true detectable in 2 JS lines. Use SeleniumBase UC mode to remove. Stock Selenium is dead against Akamai in 2026. Still valid for non-protected targets.navigator.webdriver. Auto-solves many CAPTCHAs. Good for Kasada, medium targets. Not production-safe against Akamai at scale.from seleniumbase import Driverscrapy-nodriver integrates with Scrapy directly. Lighter than full Playwright for medium targets.pip install botasaurusfrom camoufox.sync_api import Firefox # geoip=True: auto-aligns IP, timezone, locale, WebRTC simultaneously with Firefox( geoip=True– # align all 5 identity vectors to proxy exit country humanize=True– # Gaussian mouse jitter proxy={"server": "http://proxy.provider.com:8011"– "username": "user"– "password": "pass"}, screen={"width": 1920– "height": 1080} ) as browser: page = browser.new_page() # Warm up, never go directly to target URL page.goto("https://www.google.com") page.wait_for_timeout(2000) page.goto("https://cloudflare-protected.com") page.wait_for_load_state("networkidle") print(page.content()[:500])
The tools above solve the access problem. But once you have the raw HTML or JSON, you still need to extract meaning from it. That is where AI-native scraping changes everything. In 2026 the bottleneck is not access. It is the extraction layer.
Describe, don't
select
AI-native scraping replaces CSS selectors with natural language. A 2025 NEXT-EVAL benchmark showed LLMs hit F1 > 0.95 on structured extraction when input is properly formatted.
/interact endpoint clicks, fills forms, extracts behind dynamic content. SAP, Zapier, Deloitte.app.scrape(url) | app.crawl(site) | app.search("query")result = await crawler.arun(url)SmartScraperGraph(prompt="...", source=url)pip install webclawr.jina.ai/{url} is the entire API. Returns clean Markdown. Dynamic content handled via built-in rendering. Free tier available, paid ~$0.002–$0.01/page.import asyncio from crawl4ai import AsyncWebCrawler from crawl4ai.extraction_strategy import LLMExtractionStrategy from pydantic import BaseModel # Define exactly what you want, LLM extracts it, no selectors needed class Product(BaseModel): name: str price: float model_number: str brand: str async def extract(url): strategy = LLMExtractionStrategy( provider="openai/gpt-4o-mini"– schema=Product.model_json_schema(), extraction_type="schema"– instruction="Extract all products with prices and model numbers" ) async with AsyncWebCrawler() as crawler: result = await crawler.arun(url=url– extraction_strategy=strategy) import json return json.loads(result.extracted_content) # F1 > 0.95 on well-structured pages, NEXT-EVAL benchmark 2025
What it ships:
• Stealth via
curl_cffi TLS impersonation + rotating proxies
• Auto-discover REST + GraphQL endpoints on any site
• Record a flow once, export it as a runnable Python crawler
• Smart extraction with any OpenAI-compatible LLM (free tiers + local Ollama work)
• MIT licensed
One command:
uvx crawilfy-mcp-server
Why this matters: at $0.002–0.01 per request, commercial scraping APIs compound fast on any non-trivial AI agent. Crawilfy brings the full stack in-process: TLS impersonation, proxy rotation, LLM extraction, all from within your IDE. The alternative is paying per-request at scale.
From extraction to production-grade data
Crawl4AI and Firecrawl get you semantic understanding out of an LLM. But ask an LLM for a price across 10,000 articles and you will get $40, 40 dollars, 40 USD, "forty dollars", and occasionally null. Production pipelines cannot ingest that. The fix is to separate the two concerns LLMs conflate: semantic understanding and structural guarantees.
# pip install instructor pydantic anthropic from pydantic import BaseModel, Field import instructor, anthropic class JobPosting(BaseModel): title: str company: str salary_min_usd: int | None = Field(description="Floor of salary range in USD") salary_max_usd: int | None years_experience_min: int location: str remote: bool client = instructor.from_anthropic(anthropic.Anthropic()) result = client.messages.create( model="claude-sonnet-4", response_model=JobPosting, messages=[{"role": "user", "content": scraped_html}], max_retries=3, ) # result is a validated JobPosting object, not a string # If LLM hallucinates "competitive" for salary, Instructor retriesWhy this beats raw LLM calls: normalises currencies, units, and phrasings ("just under two percent" → 1.8); rejects hallucinated dates that don't fit the schema; retries automatically on type errors; gives you a real Python object downstream.
Use classical NLP when: scraping millions of consistent documents (e-commerce, classifieds), schema is fixed, domain doesn't shift, latency budget is <5ms per doc.
Use LLM + Instructor when: messy heterogeneous sources (news, newsletters, job boards), context disambiguation matters ("Apple" the company vs the fruit), schema may evolve, semantic equivalences need resolving ("FTE" = "full-time" = "permanent" = "direct hire").
Hybrid in production: classical NLP pre-filters and tags. LLM resolves only the ambiguous cases. This is what Bloomberg, Reuters Refinitiv, and FactSet actually do, not pure LLM pipelines.
Sources and further reading: Federico Trotta, The Web Scraping Club, May 2026; Instructor library.
date | None field with Instructor's retry logic catches this, the LLM has to either find a real date or return None. Without schema validation, fabricated dates pass into your database as facts.
Making LLM extraction production-reliable: the parts that are engineering, not prompting
div.qElViY from a Wix or Chakra build) that changes on the next redeploy. Instruct it to prefer selectors in order of durability: (1) JSON-LD structured data, a declared schema contract, (2) data-testid attributes, added for automation and rarely changed, (3) id attributes, (4) semantic elements (h1, time, address, article), (5) itemprop / schema.org attributes that are part of a public SEO contract, (6) named platform class prefixes (tn-, ot_), and only last (7) visual styling classes, the first thing to break on a redesign. The higher the selector sits, the longer it survives, so a self-healing loop that prefers the top of this list heals far less often.<style>, <svg>, <noscript>, and inline scripts (but keep <script type="application/ld+json">, that is data, not code), which typically shrinks the HTML three to five times. When even the clean page will not fit, do not truncate blindly, give the model targeted tools over the DOM instead, downloading the HTML to a file and exposing helpers like count_selector, dom_excerpt, and find_repeating_blocks so it can answer questions about structure without loading the whole document. And separate the two jobs: the model produces the selectors, your deterministic code does the actual field extraction, so the expensive model never has to see every row it is pulling.href when you needed the link text), and template mismatch (the sample pages the model saw do not represent every template on the site). Feeding back "the selector is too narrow, find a parent element" produces a far better second attempt than "try again", and it is the diagnostic layer that the circuit-breaker and correction-loop from the architecture section depend on. The same source also flags two JSON-LD traps worth pre-empting: server-side frameworks sometimes inject HTML comment markers (<!---->) inside the script tag that make json.loads fail silently, so strip comments before parsing, and JSON-LD can be present on some pages of a site and absent on others, so a CSS fallback is mandatory rather than optional.
Serving scraped data to an agent: the naming problem replaces the scraping problem
The agentic browser stack is 8 layers, not one product
"Browser agent" sounds like a single tool. It's a stack. Most teams building AI agents account for one or two layers; the reliable ones map all eight. When an agent fails a task, the cause is usually not the framework everyone debates, it's one of the other seven layers nobody mapped. The proxy layer sits at the bottom and every layer above it still has to reach the live site.
When an agent writes your scraper ten failure modes, from reading one end to end
The toolchains that let an LLM build a production scraper for you stopped being demos this year. The most complete public example is Zyte's open-source claude-skills plugin (v0.2.3, fifteen SKILL.md files): a staged pipeline that explores a site, negotiates a schema with you, generates a scrapy-poet project with real page objects, and deploys it to Scrapy Cloud. I read the whole repository, ran it, and wrote up thirty-four findings. The bugs themselves will be fixed and are not the interesting part. The shape of the failures is, because it is not specific to one vendor. If you let an agent build scrapers, in any framework, these ten are the ones to check for.
@handle_urls(domain) and typed Returns[ItemClass], separate from the crawl loop; the generated project ships an executable web-poet fixture suite; the schema is negotiated with a human before any code is written. A pipeline that gets the architecture right and the instrumentation wrong is a far better starting point than the reverse. Everything below is about the instrumentation.
page.on("response") listener is registered anywhere in the repository. The entire harvest is one line: html = await page.content(). Every JSON, XHR and GraphQL response the page fetched to build itself is discarded the moment it is used. The HTML cleaner then strips <script> before the model sees anything, which deletes __NEXT_DATA__, __NUXT__ and self.__next_f — the exact places modern sites park their data.Why it generalises: the stage that chooses between a private JSON endpoint and a headless browser is the stage that has been made blind to the endpoint. The browser decision is committed before anything could have argued against it.
Rule: recon captures the network, not the document. If your agent cannot show you the response bodies it saw, it did not choose between HTTP and browser — it defaulted.
networkidle and snapshots; the only scroll-related call sizes the screenshot afterwards. The hosted rung sends {"browserHtml": true, "screenshot": true} with no actions array, so the provider's own scrollBottom and waitForSelector are never requested. Grep the repo for mouse.wheel, scroll_into_view or wait_for_selector and you get nothing.Why it generalises: on any infinite-scroll listing the agent negotiates a schema against the twelve items it happened to see, and the spec looks perfectly reasonable.
Rule: the recon fetch must be at least as capable as the production fetch. Prove it on a lazy-loaded page before you trust a generated spec.
robots and you get exactly two lines: the downloader sets "ROBOTSTXT_OBEY": False, and the generated project template sets ROBOTSTXT_OBEY = True. Nothing fetches robots.txt while the spec is being built, nothing checks the captured URLs against it, and nothing warns about the mismatch.Why it generalises: the spec is built from pages the shipped spider may be forbidden to fetch, and you learn that in production, on someone else's schedule.
Rule: pin one policy across recon and production and assert it at spec time. A legal posture that changes between the prototype and the deliverable is not a posture.
Scrapy/<version> (+https://scrapy.org), because nothing sets Scrapy's USER_AGENT or DEFAULT_REQUEST_HEADERS on the default path — the one user-agent constant in the settings file is provider attribution, not target-facing identity — and transparent mode is off by default. No Accept-Language, no browser Accept. Combined with ROBOTSTXT_OBEY = True, the very first request to every new domain is a robots.txt fetch from that identity.Why it generalises: every agentic toolchain uses a heavy client to look and a light client to run. The gap between them is where the block lives.
Rule: whatever built the spec must not be more capable than what runs it. Test the fingerprint the deliverable actually ships with.
group_links[:max_links]. The next stage iterates the truncated list and writes only {url, text} — the count is discarded entirely, so nothing downstream can detect that truncation happened even in principle. That shortened list is copied into the values file, then into the spec, then verbatim into the test fixtures.Why it generalises: every agent pipeline trims lists to fit a context window. The bug is not the trim, it is that the trim leaves no scar.
Rule: carry the true count next to every sample an LLM is shown, and fail loudly when the sample and the count disagree.
examples, and the fixture converter stamps the model's own output as the assertion target. It also writes the whole values dict, including fields the page object was never asked to produce, because it is never told which fields the page object emits.Why it generalises: a suite where the model writes both the code and the expected output is a regression detector, not a correctness test. Useful, but it will happily lock in a wrong answer.
Rule: the human-corrected value is the only ground truth an agentic pipeline has. Wire it into the assertion, or admit you have no test.
threshold, baseline, previous job or expected items and you get nothing.Why it generalises: a run that falls from 40,000 items to 4,000 with every field populated passes every check. That is the most common real failure in production scraping and it is invisible to a level-based test.
Rule: coverage is a delta, not a level. Compare against the last good run and alert on the derivative.
captured_at, http_status, full headers, final_url on redirect, and the backlinks a page was discovered from. None of it survives into the output. The item class is built purely from the schema, the spider yields await page.to_item() and nothing more, and there is no item pipeline hook that could stamp anything afterwards.Why it generalises: three months later the price field goes null and the only question that matters is "which URL, fetched when, with what status". If the row does not carry it, the answer is a re-crawl.
Rule: every row ships source URL, fetch timestamp and HTTP status. It costs three columns and it is the difference between a diagnosis and a shrug.
Why it generalises: this is the classic monitoring inversion, and agentic pipelines make it worse because the agent narrates the result to a human in confident prose.
Rule: "no news" and "could not ask" must be different states with different messages. If they collapse, your monitoring is a comfort blanket.
Authorization: Basic <b64(apikey)> to whatever host it names — no scheme check, no host check, no allowlist; grep for urlparse, netloc, hostname or allowlist across the repo returns zero validation hits. Separately, the human-approval channel is a local HTTP server whose do_POST reads the body and writes it straight to the output path: no Origin check, no Host check, no shared secret, no size cap, no content-type check, and its logger is stubbed out. The only gate is an ephemeral port written in plaintext into a JavaScript file.Why it generalises: an agent that reads web pages can be argued with by a web page. If it can also be handed a URL that its credentials follow, the injection has somewhere to go.
Rule: allowlist the hosts your credentials are permitted to touch, and treat any local approval channel as an unauthenticated endpoint on the user's machine, because that is what it is.
@handle_urls(domain) decorator and Returns[ItemClass] base that a previous step had carefully constructed with libcst. Both are the same class of defect — a later stage that does not read what an earlier stage produced — and it is the defect agentic pipelines are structurally most prone to, because each stage is a fresh context that only knows what it was handed.
Test for properties, not for examples. The sharpest statement of the model-authored-oracle problem I have seen came from someone whose agent shipped 127 passing tests and a clean linter, including a test named after the exact promise it was violating: two distinct items never collide. The function was not injective, and the suite never looked, because it exercised the hostile inputs somebody thought of rather than searching for a counterexample. A green suite is evidence that the cases you encoded ran, and nothing more. Where a field carries a real property — uniqueness, monotonicity, a total ordering, a stable key — property-based testing searches the space instead of sampling your imagination, and it is the one addition that makes a machine-authored suite mean something.
Make the human correction executable. Wherever a person corrects a value, that corrected value must become an assertion, not a comment and not context. If your pipeline has exactly one human-verified fact in it, spend it on the test.
Emit provenance by default, strip it later if you must. Source URL, fetch time, HTTP status, and the selector or page-object version that produced the row. Four fields. They are the entire difference between debugging and guessing.
Alert on the derivative, not the level. Item count versus the last good run. Field fill-rate versus the last good run. Response-code mix versus the last good run. A threshold against zero catches the failure that was never going to be subtle.
Keep one identity policy end to end. The same user agent, the same header set, the same robots posture in recon and in production. Where they must differ, make the difference explicit and asserted, so nobody discovers it from a 403.
Treat every stage boundary as a lossy channel. The characteristic agentic bug is a later stage reading a truncated, reformatted or regenerated version of what an earlier stage knew. Carry counts alongside samples, carry file hashes alongside paths, and have the last stage assert that what it received still matches what was produced.
Threat-model the agent's own tools. Host allowlists on anything that carries a credential; authentication on any local approval server; a size cap on anything that writes to disk. The agent reads untrusted text for a living.
And keep the good part. Page objects, a negotiated schema, an executable fixture suite and a surgical repair path are genuinely the right architecture for machine-authored scrapers. The lesson from reading this end to end is not that agents cannot build scrapers. It is that they build the code well and the instrumentation badly, and the instrumentation is what you will be living with.
When DIY cost exceeds platform cost, these services handle the heavy lifting. Each solves a specific problem, choosing the right one depends on which wall you are facing and at what scale.
A shift worth watching: browser agents are becoming coding agents. The first generation of browser agents worked by translating the page into something a model could digest, flattening the DOM, the accessibility tree, a screenshot and browser state into a simplified representation and asking the model to choose an action against it. That representation is lossy by construction, and every extraction is bounded by what the harness decided to include.
The newer approach inverts it. Rather than handing the model a summary of the page, hand it the Chrome DevTools Protocol directly and let it write its own low-level browser code: explore, form a hypothesis about where the data lives, write the query, extract exactly what it needs. The team behind one of the most widely used agent frameworks describes this as their current direction, and reports two related observations, that stronger models now rarely reach for screenshots, and that they prefer working against structure over pixels.
The reason this belongs in a scraping guide rather than an agent guide is the economics it implies. An agent that writes code once and runs it is doing the compile-and-run pattern described elsewhere in this section, arriving at it from the other direction. An agent that reasons over a screenshot on every page is paying model inference for every row it extracts. The first scales, the second demos.
The other way to slice it: the four layers of the runtime loop
The newer approach inverts it. Rather than handing the model a summary of the page, hand it the Chrome DevTools Protocol directly and let it write its own low-level browser code: explore, form a hypothesis about where the data lives, write the query, extract exactly what it needs. The team behind one of the most widely used agent frameworks describes this as their current direction, and reports two related observations, that stronger models now rarely reach for screenshots, and that they prefer working against structure over pixels.
The reason this belongs in a scraping guide rather than an agent guide is the economics it implies. An agent that writes code once and runs it is doing the compile-and-run pattern described elsewhere in this section, arriving at it from the other direction. An agent that reasons over a screenshot on every page is paying model inference for every row it extracts. The first scales, the second demos.
The injection surface that never crosses the network. Anyone pointing an agent at the open web should understand one delivery mechanism in particular, because it defeats the obvious defences. Everything after the hash in a URL is the fragment, and the fragment is never sent to the server. It stays in the browser. That means no network appliance, no WAF, no proxy log and no server-side filter can inspect it, because none of them ever receive it.
The attack writes instructions into that fragment, appends it to a link to a genuinely trustworthy site, and distributes the link normally. The page loads clean because the page is clean. When the reader asks an assistant built into the browser to summarise what they are looking at, the assistant reads the full URL, fragment included, and treats the text it finds there as part of what it is working on.
Two rules follow, and they apply whether you are building an agent or using one. Content retrieved from the web is data, never instruction, and that includes the URL itself rather than only the page body. And an agent with both browsing and any acting capability needs those separated by something more deliberate than the model's own judgement, because the model is being handed the attacker's text through a channel your infrastructure cannot see.
The CAPTCHA reality check: the LLM in your agent is almost never the thing solving it
The attack writes instructions into that fragment, appends it to a link to a genuinely trustworthy site, and distributes the link normally. The page loads clean because the page is clean. When the reader asks an assistant built into the browser to summarise what they are looking at, the assistant reads the full URL, fragment included, and treats the text it finds there as part of what it is working on.
Two rules follow, and they apply whether you are building an agent or using one. Content retrieved from the web is data, never instruction, and that includes the URL itself rather than only the page body. And an agent with both browsing and any acting capability needs those separated by something more deliberate than the model's own judgement, because the model is being handed the attacker's text through a channel your infrastructure cannot see.
hcaptcha.com, cross-origin to the host page. The solve happens entirely inside that frame, and when it finishes the widget writes a token into a hidden textarea[name="h-captcha-response"] the host form submits. An agent driving the outer page over Playwright or CDP has full control of the host but only limited reach inside the widget frame, because the same-origin policy blocks it. A browser extension, which runs with cross-origin privileges, can observe and click inside the widget frame. That asymmetry, not model intelligence, is why extension-based solvers work where a page-driving agent stalls.Agentic browsers
the visitor that reads, decides, then clicks
A category that did not exist in a serious way two years ago and now sits on both sides of this guide: it is the thing you might build with, and it is the thing arriving at your target. Worth its own section because the economics, the failure modes and the threat model are all different from headless automation, and most of what is written about it is either marketing or panic.
Four categories, and they are not competing with each other
People argue about these as if one wins. They serve genuinely different jobs, and the distinction that matters is who the browser is for and whose session it uses.
The cost is the tool surface, not the model
This is now the best-evidenced claim in agentic browsing, with five independent measurements from five teams using five methods. It is also the one most likely to save you money this week, because on most frameworks it is a configuration change rather than a rewrite.
The security problem is architectural, not a bug queue
The worked example is a line of hidden text in a product review saying to call
updateEmail with the attacker's address. And it chains: read cart, apply coupon, change address, check out. Named vectors to worry about are anything the agent ingests without you writing it — reviews, emails, fetched pages, PDFs, support tickets.That matters because the first thing most teams reach for is a filter that strips known injection phrases from the text before it reaches the model. Against a canvas payload there is no text to strip. Any defence that operates on the DOM string is structurally blind to it, which is the clearest argument going for putting the control on the action rather than on the input.
1. Register fewer tools. Expose only what an agent genuinely needs and delete the discretionary ones. This is also the change that makes the agent cheaper and, per Vercel's numbers, more accurate. Rare alignment between security and performance, so take it.
2. Put consequential actions out of band. The good formulation: make it something the agent narrates but cannot click. A human confirmation the model has no tool to satisfy is the only control an injection cannot argue its way past.
3. Re-validate server-side on every call. Permissions and ownership, every time, treating each request as potentially hostile. The agent's session being valid tells you nothing about whether this particular call was the user's idea.
4. Scope the session. Narrow the agent's access to what the task needs, with short-lived tokens rather than the user's full standing authority.
5. Rate-limit and log per tool. This one is for afterwards. It will not stop the first incident, it is how you find out it happened and what it touched.
And the question to sit with if you run agents: would you let one browse arbitrary pages using the same account that holds your production access? Most teams answer no immediately, and then discover their agent does exactly that.
The operational tax nobody scopes
Agentic browsing demos beautifully and degrades quietly, which is the worst combination for a project plan. Three failure modes that show up after the pilot.
task_id, step, timestamp, action, outcome and a confidence score, so a decline is visible as a trend rather than an anecdote. Screenshot traces for debugging, because reconstructing an agent's reasoning from text alone is miserable. Outcome validation before anything progresses down the pipeline. Credentials in a vault or environment, never in the agent's context. And treat the agent as an untrusted endpoint on your own network: scope its file access to the task, and monitor its session the way you would a third party's.
What this means if you scrape rather than build agents
navigator.webdriver and CDP detection went soft, and why detection is moving to behaviour, intent and network identity. For a scraper the practical read is that "look human" is a less useful goal than "make coherent sense as some legitimate client", which may not be a human at all.When DIY cost
exceeds platform cost
If spending more than 2 engineer-days/month on anti-bot maintenance, a managed platform is cheaper. Crossover typically hits when facing F5 Shape or Kasada at scale.
The best option if you don't want to build scrapers yourself. Apify is a cloud platform where scraping is already done for you, 10,000+ community-built Actors cover almost every major website: Amazon, LinkedIn, Instagram, Google Maps, TikTok, Zillow, Twitter/X, Google Search, and thousands more. You pick an Actor, give it a URL, and get back clean JSON. No Python, no proxies, no infrastructure.
- You need data from a well-known site quickly
- You don't want to maintain scrapers long-term
- You're building an AI agent that needs live web data
- You want someone else to handle anti-bot bypasses
- You need to scale without managing infrastructure
- Your target site has no existing Actor
- You need custom data transformation logic
- You're scraping at very high volume (cost)
- You need full control over request patterns
- Data stays internal and can't touch third-party cloud
act(), extract(), observe(), agent(). Write browser flows in plain English ("click submit button") that survive page redesigns via runtime LLM resolution. Built on CDP, supports OpenAI/Anthropic/Gemini. 65% Mind2Web benchmark. Self-healing + auto-caching. TypeScript and Python.The next CAPTCHA frontier is liveness, and it is being defeated the same week it ships
getUserMedia stream, so the model never sees a lens, it sees whatever frames you hand it. This is a presentation or injection attack, and it is the same class of bypass that has dogged face liveness for years (masks, screen replays, injected deepfake video). A hand is, if anything, easier to synthesise than a face, with fewer micro-expressions to get right. Reported virtual-camera and deepfake liveness-bypass attempts rose sharply through 2025-2026 as the tooling commoditised, with some injection kits priced around the cost of a coffee.Computer Use Agents when scraping isn't enough
A new category emerged in 2025: AI agents that don't just scrape, they log in as the user, navigate any UI (web apps, legacy portals, desktop software), handle MFA and CAPTCHAs, and return structured JSON. Different from scrapers because the user grants permission, "Plaid for any website." If your problem is utility bills, payroll exports, e-commerce backends, or any portal without a public API, this is the category.
Platforms sort out the browser and the fingerprint. But every request still needs an IP address, and the type of IP matters as much as any other signal in your stack.
IP type matters
more than provider
Rotating proxies is table stakes. The real variable is IP type, datacenter IPs score near-zero on DataDome and PerimeterX regardless of fingerprint quality.
github.com/fabienvauchelles/scrapoxygeoip=True in Camoufox to align all five vectors automatically.http:// and https:// keys must use http:// scheme. Using https:// causes BoringSSL WRONG_VERSION_NUMBER (TLS-over-TLS failure). Fix: "https": "http://key:@proxy.crawlera.com:8011/"from curl_cffi import requests import time– random session = requests.Session(impersonate="chrome124") # Crawlera/Zyte: BOTH keys use http://, never https:// PROXIES = { "http": "http://apikey:@proxy.crawlera.com:8011"– "https": "http://apikey:@proxy.crawlera.com:8011"– # http:// not https:// } def fetch(url– retries=3): for i in range(retries): try: r = session.get(url– proxies=PROXIES– timeout=30– verify=False) # verify=False: proxy cert if r.status_code == 200: return r if r.status_code in (403–429): time.sleep(2**i + random.uniform(0–1)) except Exception as e: print(f"Error: {e}") return None
Rotate sessions, not IP addresses: stickiness is the strategy
sec-ch-ua or Accept-Language between requests that claim to be the same browser. Between sessions, rotate within the same geo and ASN cluster, since a jump from a Tokyo residential IP to a Toronto one reads as a VPN switch, not a returning user. And separate your retry cases: a transient timeout retries on the same session after a short jittered backoff, only a clear block (407, 429, a CAPTCHA) flags the session and forces a fresh one. At fifty machines this needs a shared store (a Redis-backed pool) so every worker sticks to the same session instead of each inventing its own.Know where your IPs come from, and remember you are a guest
What a compromised residential pool actually looks like from the inside
Verify the geography, do not buy it. Where a provider says an IP lives and where it answers from are different claims, and the industry lets the first stand in for the second. Location data for most networks comes from geofeeds the operators publish about themselves, which is a self-declaration with no verification step in front of it. Measure instead. Ping the addresses from a distributed measurement network and compare the latency to what the claimed location would require, and a familiar pattern appears: addresses advertised across dozens of countries resolve to a handful of large datacentre regions, with Northern Virginia, Amsterdam and Singapore absorbing most of it. Asia is the worst offender, where an address sold as Indonesian or Australian is very often answering from Singapore because the bandwidth is cheaper there.
This matters beyond truth in advertising. The five-vector coherence test at the top of the network layer assumes your exit is where you think it is. Set a timezone, an Accept-Language and a DNS resolver to match a country your traffic is not actually leaving from, and you have built a contradiction carefully and deliberately. Before you tune the four vectors you control, measure the one you bought.
The risk that runs the other way: your IP is liable for the whole pool
This matters beyond truth in advertising. The five-vector coherence test at the top of the network layer assumes your exit is where you think it is. Set a timezone, an Accept-Language and a DNS resolver to match a country your traffic is not actually leaving from, and you have built a contradiction carefully and deliberately. Before you tune the four vectors you control, measure the one you bought.
You now have the full picture: detection layers, six anti-bots, sixty libraries, managed platforms, proxy types. This section collapses all of it into a single decision tree you can follow for any target site.
Walk this in order.
Stop at first win.
Each step adds complexity, cost, and maintenance. Most production scraping is solved at steps 1–3. Never start at step 5.
SSL_read/SSL_write directly. If you find the API endpoint, every HTML anti-bot becomes irrelevant.__NEXT_DATA__. React SPAs often have >50KB script containing all data. Confirmed: Grainger.com (DataDome-protected), 110KB JS state blob bypasses DataDome entirely because it's in initial HTML.curl_cffi with JA4 impersonation resolves most Akamai and DataDome at HTTP layer. Add residential proxy. If __NEXT_DATA__ appears in response, extract it with chompjs.Quick reference cheat sheet
| Anti-bot | Primary vector | Steps 1–2 viable? | Best tool | Key note |
|---|---|---|---|---|
| Akamai | JA4+ + sensor.js + extension probes | Often | curl_cffi + CloakBrowser | Find mobile/GraphQL first |
| Cloudflare | JA4 Rust edge + Turnstile | Sometimes | Camoufox | Origin IP via SecurityTrails |
| DataDome | 85K ML + WASM boring_challenge | Yes | curl_cffi + mobile IP | Check __NEXT_DATA__ first |
| PerimeterX | 5-vector score | Sometimes | Camoufox + residential | Fresh session per domain |
| Kasada | Polymorphic JS PoW | Rarely | PatchRight + residential | Never playwright-stealth |
| F5 Shape | Custom VM + minute expiry | No | Managed API | DIY not practical |
If your target sits on a Sometimes, Rarely or No row, email me at scrapesync@gmail.com before you lose a weekend to it. Send the URL and a note on what you have already tried, and I will tell you which rung it is actually on and what I would try next. I usually reply within a couple of days, slower when I am mid-job. Most of the time the answer is a section further up this page and I will point you at it. Sometimes it is one of the managed APIs in the Best tool column, in which case I will name Zyte or Scrapfly rather than myself. The interest I should declare, since this is a table of vendors and I sell on the same rung: scraping is my day job, I run
What practitioners are
actually shipping in 2026
Fresh insights from engineers actively solving these problems in production. Shared publicly on LinkedIn.
Layering is the norm, not the exception. Most fashion sites run three or more barriers at once, sitting at different levels: the connection, the request rate, the rendering, the behaviour. Any one of them is minor. Together they turn a fetch into sustained engineering, which is exactly what pushes the sector up the scale. Roughly a third of fashion sites still resolve as Easy — but more than half land at Moderate or above and better than one in four reach the tiers that need residential proxies and heavier infrastructure.
The split inside the sector is the useful bit. At the premium and heritage end the defences are quiet: an enterprise bot-management platform in the background, no CAPTCHA to interrupt the shopper, and a block returned on the very first request rather than a polite request to slow down. High-volume fast fashion runs lighter, leaning on basic rate limiting. The rule underneath: the closer a brand sits to scarcity and price protection, the heavier and quieter its controls. Much of the machinery now standard across the sector was proven on limited sneaker drops, where a shoe sells out in seconds and resells for multiples — queues, waiting rooms and behavioural detection were built to hold off automated buyers, then generalised.
The figure that says where the anxiety actually is. Only 4% of fashion sites name an AI crawler to block in robots.txt, a fraction of the rate in news and publishing, and about 69% publish a robots.txt at all — with most of the attention still on Googlebot rather than GPTBot. A newspaper worries about a model trained on its archive. A fashion retailer is largely indifferent to whether a chatbot read its About page, because its concern is commercial data: prices, ranges, stock levels. The industry that lives on reading demand signals understands exactly what its own signals are worth.
And the operational warning, which corrects a reasonable assumption. On these sites full access is required from the very first request, and the rate limiting responds to client identity rather than speed — so slowing down will not help you. That is the opposite of the 429 case elsewhere in this feed, and telling the two apart before you start tuning is the whole game.
Which is why “how hard is the web” is the wrong question. Difficulty skews violently by sector, because the barrier tracks commercial value rather than technology. Fashion is the single hardest industry in the report, needing moderate-or-harder infrastructure on 57% of its sites; reference and publishing sit at the opposite end. The barrier you actually meet has less to do with the web in the abstract than with which corner of it pays your bills.
The line worth pinning above a desk. “Web scraping at scale is getting more and more complex. It’s not just a script kind of problem; it needs to be a system” — proxy management, concurrency, a decision about whether the page needs JavaScript and a fallback when it does, and cost managed across all of it. “It’s all about orchestration and instrumentation.”
Read the signals as different kinds of thing, not one wall. CAPTCHA is close to binary — “we want you to be gone”. JavaScript either loads or it does not, and plenty of sites run it purely for user experience while incidentally making you pay for a headless browser. Rate limiting is neither: it is a dial, not a switch — “please, yes, you can access this automatically, but don’t rush”. So the operational question is not is this site blocked but how many of these mechanisms is it running, and which kind. A CAPTCHA changes your answer; a little client-side JavaScript might not.
And the finding that surprised the vendor who commissioned it. Anti-bot adoption came in at just 18.5%, against their own instinct — because their day-to-day book of business is exactly the sites that run it. “It’s sites with e-commerce, pricing intelligence, data that correlates deeply with commercial value.” That is a vendor stating plainly that its own sample is not the web, which is rarer than it should be.
Two methodology notes that earn the rest of it trust. Every “website” here is one landing page, hit once, from a single IP — “what’s measured is the front door, not the whole building”, so barriers deeper in a site or visible only from certain regions are simply not in the data, and they say so. And they deliberately did not run the audit through their own unblocking API, because it would have solved every barrier and erased the measurement: “we wanted to capture the web as it is, just a simple request and response, and not taint the process of getting it.” Set that against the benchmark card in this feed — this is what it looks like when a vendor builds the instrument to survive its own result.
Why this is the same finding as the audit above, from the other end. That research separates the signals by kind: CAPTCHA is binary, JavaScript either runs or does not, and rate limiting is a gradient — the site is not refusing you, it is pricing your pace. A 429 is a negotiation. A 403 is a verdict. Treating them as the same event is how you end up rebuilding a TLS profile to solve a problem that a
sleep would have solved.The rule. Read the status before you reach for a technique. 429 means slow down, honour
Retry-After, and back off with jitter — your identity was fine. With one caveat this feed earned the hard way: some sectors rate-limit on client identity rather than pace — the fashion card in this feed found exactly that, with a block on the very first request and no amount of waiting helping. So the test is whether backing off actually changes the outcome. If a slower cadence gets the same 429, the limiter is not counting your speed, it is counting you, and you are back to the 403 question of which layer decided. 403 means you were judged, and now the question is which layer judged you, which is a different card in this feed and a different afternoon. The expensive mistake is not picking the wrong fix; it is never asking which problem you had.The lifecycle is the part worth copying, because a memory that only grows is a liability. Skills are widened when a later run discovers more (a country filter the first agent never tried), gated by safety and topicality checks, provenance on whoever produced the session, and human review before anything reaches a public catalogue. Each carries success and failure counts and is automatically demoted when it drops below threshold. Knowledge that stops being true gets retired rather than quietly poisoning the next run — which is the same discipline the self-healing card in this feed argues for, applied to memory instead of code.
Why it changes the bill and not just the latency. The claim is that an open-weight model can then handle site tasks that would otherwise need a frontier one, because following a proven recipe is instruction-following rather than exploration. That is this guide’s own principle from a new direction: spending the model once at build time is cheaper than spending it on every request — and here the “build” is done by whichever agent happened to go first, on behalf of everyone after it.
The caveat to hold. A shared recipe pool across tenants is a shared blast radius: a skill that encodes a site quirk is fine, a skill that quietly encodes an assumption that has since changed is a wrong answer delivered fast and cheap. The failure counters are the control, so the number that matters when evaluating any system like this is how quickly a stale skill is demoted, not how many skills it holds.
Cloudscraper itself is the right specimen for that write-up. It extends
requests.Session with logic for older Cloudflare JavaScript challenges — which is to say it is a transport-level answer to a problem that moved years ago. This guide has made that case repeatedly: a tool that solves the challenge of 2019 tells a 2026 detector exactly what it is, because the giveaway is the handshake underneath rather than the challenge logic on top.Why it belongs in this feed. Two things follow from a defender writing this down. The first is practical: the countermeasures on the sites you meet are increasingly informed by documentation of your specific tools, by name, so the half-life of any named bypass is set by how fast that documentation circulates. The second is about posture. The targets in that write-up are public university services — not a retailer with a pricing team, but a body with a duty to stay reachable and a small budget for standing back up. It is a reasonable moment to check that the thing you point at a
.edu is a considered choice rather than the same profile you use on a marketplace.The four checks named, all boring and all skipped in practice: workload isolation, file-access limits, input validation, and agent permissions. The last is the one that changed recently — a parser that can read arbitrary local files is bad, but a parser sitting next to an agent that can also make network calls and write files is a different category of bad, because disclosure becomes exfiltration without a second exploit.
The tooling response worth knowing about is a category rather than a product: open-source runtime guardrails that monitor agent actions, record session artifacts, and enforce policy on risky operations. The design principle transfers cleanly even if you never adopt one — separate agent logic from security policy, log what the agent actually did, and make sensitive operations reviewable after the fact. Prompt engineering is not a control surface once an agent holds credentials, because the thing you are defending against is not in the prompt.
The four practices asked for are the ones a competent collector already wants for its own reasons: conditional requests so unchanged resources cost a 304 rather than a body, incremental crawling against a stored cursor instead of full sweeps, deduplication before fetch where several URLs resolve to one artifact, and skipping unchanged content outright. Every one of those lowers your bill as much as theirs, which is what makes the ask reasonable rather than moralising.
The strategic read. Blocking used to be a response to who you were — wrong fingerprint, wrong ASN, no permission. This is a block for how you behaved while doing something permitted, and it is a much harder one to route around, because there is no identity to improve. Combined with the metering and licensing described elsewhere in this feed, the direction is consistent: access is becoming conditional on cost imposed. The cheapest insurance is to be the crawler nobody has a reason to notice.
The fields named for a portable session map almost one to one onto the row-level list: the source URL, the raw capture, the timestamp, the model and prompt version, the extracted fields, and the sequence of steps that produced them. The two additions over the row-level case are the interesting ones — model and prompt version, because the same question asked next month against a different checkpoint is a different experiment, and the step sequence, because with an agent the path is part of the result in a way it never is for a deterministic extractor.
And a cost note from the same source that pairs with it. Prompt caching is an extraction-cost lever, not just a latency one: keep instructions and schemas as a stable prefix and put the changing content at the end, so repeated work hits cache rather than re-billing the same tokens. That is the same shape as the agent-cost finding elsewhere in this feed — the spend is decided by how you arrange the context, not by which model reads it.
Driving the real entry point changed the answer. The true count was four, not three, because the paginator's
COUNT(*) was invisible from where the benchmark stood. That gap sounds trivial and is not: one query in four is nothing in a latency budget, but an assertion written against the wrong number fails immediately and forever on correct code, which is how a well-meant regression test gets deleted by the next person.The replacement is the transferable idea. Not a benchmark that prints a number, but a query budget — a fixed count asserted against the real endpoint at two different data sizes, so flatness is proven rather than assumed, with each query named in a comment so any future change has to be justified rather than absorbed. A benchmark prints a number. A regression test refuses to let it change.
Why this belongs in a scraping guide. Substitute your own instruments and the argument survives intact. A health check that builds its own request instead of running your scraper's actual client is measuring the library, not your pipeline. A coverage alert whose fixture was captured once and never re-driven is measuring history. The guide already says to check the instrument before trusting the verdict; this is the procedure that operationalises it — delete the thing the check protects and watch it go red. If it does not, you have been reassured by nothing for however long it has been green.
Three observations from the practitioner reaction worth keeping. First, it is suing partly on another company's behalf, while that company litigates separately in New York. Second, the obvious remedy is available and unused: put it behind a login, as every site serious about stopping collection eventually does. It stays open because free and ad-supported is the business — open to humans to sell ads, closed to bots so nobody builds a competing index. Third, and least legal: the asymmetry in costs means a smaller defendant can lose by winning slowly.
Why it matters beyond one company. The DMCA-circumvention theory is the live threat to public-data collection, and it is a different animal from the CFAA arguments that keep failing. Access theories ask whether you were authorised. A circumvention theory asks whether you got past a technical measure — which every practice in this guide, by construction, does. The judge killing the broad version and the narrow one still being attempted is the shape to watch: the doctrine gets built out of the narrow attempts. Read alongside the stealth-bot legislation covered elsewhere in this feed — two different routes to the same destination, one through the courts and one through statute.
Two numbers to sit alongside it. Cloudflare's public bot-versus-human tracker puts bots at roughly 57 to 58% of HTTP requests for HTML content against 42 to 43% human. Imperva's report on 2025 data puts bots at about 53% of measured traffic for the second year running. Different methods, different denominators, same conclusion: the majority audience for HTML is already not people.
Where it stops being comfortable. A two-version web is a cloaking arrangement, and cloaking has always been the thing search engines punish and researchers distrust — you cannot verify a claim about a page if the page you are shown differs from the page others get. It also sits uneasily beside the poisoned-font card above, which is the same capability turned hostile: once serving different bytes to different clients is normal infrastructure, serving deliberately wrong bytes is a configuration change rather than an attack. The split is not itself dishonest. It removes the guarantee that used to make dishonesty detectable.
Second, and more interesting for anyone deciding where to spend: these tools only activate for an agent that is actually driving a browser. A large share of agent traffic that matters commercially never opens one — desktop assistants and coding agents call MCP endpoints directly. For those clients, a script injected into your HTML is invisible by construction. So the feature addresses the browser-agent case precisely, and the non-browser case not at all.
Read the source carefully: this critique comes from a company selling the alternative it names, which is a reason to check the mechanics rather than to dismiss them — and the mechanics check out, because “it proxies to your own endpoint” is a claim about documented behaviour rather than a matter of opinion. Set against the adoption numbers elsewhere in this feed, where MCP server cards appeared on fewer than fifteen sites in an entire scan, the honest summary is that the plumbing is arriving faster than anything to plumb.
Why the substitution is grammatical rather than random. Garbage would be caught. The design goal is text that reads as unusual but sensible, so nothing downstream trips. At about 25% of words swapped, testing reported meaning failure in 55.8% of news passages and around half of general web text. Built by Isaque Seneda and Gabriel Abrucio with the type foundry Playtype, running since October 2025.
The number that reframes it, from the creators' own testing. Measured against FineWeb-Edu — a quality filter used to build a major public training dataset — over 90% of shielded pages get rejected outright, and the minority that survive the filter carry false meaning in roughly 19.4% of their content. That is a different and more interesting claim than “it poisons scrapers”. The primary effect is exclusion: most shielded text never makes it into the corpus at all, because a filter tuned for quality reads slightly-wrong prose as low quality and drops it. The poisoning is what happens to the remainder. For a publisher wanting out of training data that is close to an ideal outcome. For anyone collecting text, it means the damage is concentrated in exactly the pages that passed your quality checks.
What it inverts. This guide spends a lot of words arguing that a real browser is expensive and you should reach for HTTP when the data is in the HTML. Against this technique that advice is exactly backwards: a headless browser that actually renders the font reads the page correctly, and so do OCR and vision-language models working from a screenshot. Frequency analysis over a large corpus also unpicks it, and anyone holding the font file can simply reverse the mapping. So it does not stop a determined collector — it taxes the cheap path and leaves the expensive one intact, which is a deliberate and rather sharp choice of who to inconvenience.
The costs the defender eats are worth knowing, because they bound how far this can spread. Search engines index the decoy text. Screen readers hit the protected regions, which is a real accessibility harm rather than a footnote. RSS feeds leak the original text unencrypted. Any of those may matter more to a publisher than the scraping does.
What it means for your pipeline. Pair this with the plausible-wrong card above and the conclusion is uncomfortable: a value can now be wrong on purpose, in grammar, at scale. Row counts, schema checks and completeness validation all pass. The detections that survive are semantic — does this text agree with the rendered version, does this field agree with a second source, does the vocabulary of this page drift from its own history. If you collect text for a model rather than numbers for a table, this is the threat model to hold, and the cheapest insurance is spot-rendering a sample of pages you already fetched over HTTP and diffing the words.
The shape. An unblocking API fetches clean Markdown from pages that would otherwise be unreachable, lands it in object storage with metadata sidecars, embeds it, and stores vectors. Then the parts worth stealing: incremental syncs driven by content hashing so unchanged pages are not re-embedded, cited answers via retrieve-and-generate so a claim can be traced to a source, a golden-set evaluation script so a change to the pipeline can be scored rather than eyeballed, and hybrid routing that falls back to a live fetch when the best retrieved chunk falls below a relevance floor.
That last one is the design idea, and the trigger matters. It is not a staleness clock — it is the index admitting, per query, that it does not hold a good enough answer. Most RAG builds treat the index as the truth and quietly serve the closest thing they have, however far away that is. Scoring the best chunk and routing past the index when it is too weak makes freshness a runtime decision rather than a cron job's problem, and it degrades to a live fetch instead of to a confident wrong answer.
Reading the repository rather than the announcement adds the parts worth stealing. Change detection hashes the raw scrape, not the stripped body, so cosmetic boilerplate churn does not force a re-embed, and last-seen is tracked in a separate ledger from content age. A render check exists as its own step because it has to: on a JavaScript-only page a plain GET returned 96 characters and rendering returned the whole thing. Boilerplate stripping removed 21% of characters batch-wide. Near-duplicates go through MinHash/LSH. A reconcile step retires URLs that have dropped out of the source list, which is the unglamorous half of freshness nobody builds. The golden set scores hit@k and MRR rather than vibes — the six-page pilot reported hit@5 of 100% and MRR 0.88.
And it is costed, which is rarer than it should be: a six-page pilot with weekly refreshes runs about four cents a month. Small enough to be a toy, but the unit is right — cost per page per refresh is the number that decides whether an index of ten thousand pages is a line item or a project. Vendor-authored, so read the tooling choice accordingly; the architecture is independent of it.
--stealth flag. On a local fingerprint probe it scored 94/100, against 70 for standard Chromium CDP tooling and 35 for vanilla Playwright and Selenium: navigator.webdriver false, a complete window.chrome with runtime and loadTimes, a spoofed NVIDIA GPU over WebGL.The architecture is the hybrid this guide keeps recommending. Use the browser only to mint the session, then replay with a fast HTTP client. Obscura produced valid Akamai cookies —
_abck, bm_sv, bm_mi — and curl_cffi carried them to the availability endpoint for a 200. The session module retries up to six times with an eight-second settle between reloads, validating by calling the rooms endpoint and checking the cookies are actually present rather than merely set.Where the lightness costs you. Heavy fingerprinting suites never finish inside a minimal engine — boolean checks pass, but CreepJS and Pixelscan stall rather than completing. That has a real consequence: the Akamai sensor script sometimes ran incompletely, leaving
_abck in its unsolved state, marked by a -1 in the second segment, which returns 403. Camoufox, a real Firefox, did not wobble. So the trade is explicit: a tenth of the memory, most of the fingerprint, and a sensor that occasionally does not finish.And then the finding that outranks all of it. From clean residential IPs the first load sometimes worked and sometimes needed a reload or two; the determinant was address reputation rather than fingerprint quality. The proof is the detail worth stealing: requests issued from inside the page context came back HTTP 500 — a backend error, not a bot block. A 500 means your traffic reached the application. That is a cleaner pass signal than any 200, because nobody serves you an accidental stack trace to trick you.
Source: The Web Scraping Club, THE LAB #113.
Second: they are invisible in your analytics. Client-side analytics fire from JavaScript. No JavaScript, no beacon, no row. So the assistant traffic arriving at your site does not appear in the dashboard most teams use to decide what to build, which is precisely why the measured 2:1 software-to-human ratio in the earlier card surprised the people who measured it. They had to read raw server logs to find it. If your only instrument is client-side, your instrument is blind to the fastest-growing segment of your traffic by construction.
The split is the interesting part. That the US assistants skip rendering and several others do not is a cost decision, not a capability gap — rendering is expensive at fetch volume. It also means “do AI assistants execute JavaScript” has no single answer, and any advice built on one is describing whichever assistant the author happened to test. Segment by client, as ever.
The technique named for it is a two-stage check: deterministic schema and completeness validation first, then an LLM evaluating suspicious values — the author's term is “sus vals” — on whatever survives. That is a defensible use of a model in the pipeline, and notably the opposite of the pattern this guide warns about. The model is not doing the extraction, where it would be a per-request bill forever and a new source of quiet error. It is doing adjudication on a small suspicious subset, which is cheap, and where being occasionally wrong costs you a false alarm rather than a corrupted row.
The operational half. The claim attached is that over thirty days more than 99.7% of delivered runs passed those checks, with self-healing agents handling most failures automatically. Treat the number as a vendor's own, per the benchmark card above — but the definition underneath it is the transferable part: a run that fetched everything and delivered a wrong number does not count as delivered. Most teams have no such definition, which is why they do not have the metric either.
Now score the vendor benchmark against those four. It ran 15 providers over 99 targets, 5 attempts each, 7,425 billable requests, reporting a failure-aware per-target p75 latency. Results: String 97.0% at 9.98s, Scrapfly 82.0% at 18.35s, Context.dev 79.2%, Firecrawl 78.6% at 9.21s, Bright Data 78.0% at 26.14s. And to its real credit, the harness, the 99 target URLs, the pass criteria and every provider adapter are published, so anyone can re-run it. That is a higher bar than almost any benchmark in circulation and it deserves saying plainly.
What still needs holding in mind. The vendor authored the benchmark it comes first in, by fifteen points. The pass criterion — question one, the one that decides everything — lives in the repository rather than in the write-up, so the headline travels without it. The post claims 2 to 10× better cost effectiveness while the benchmark contains no cost analysis at all, which is a separate claim wearing the benchmark's authority. And the comment thread under it is three people from the same company. None of that makes the numbers wrong. It means reproducible and disinterested are different properties, and this is the first without being the second.
The rule that falls out. A published success rate is a claim about somebody else's target list at somebody else's request rate under somebody else's definition of success. The only number that describes you is the one you produce on your own sites at your own volume — which is exactly what an open harness makes possible, and the best reason to want one.
And it is not merely that cheap sometimes ties. On Amazon: ScrapingAnt at $19 returned in 13.19s, Bright Data at $300 in 4.18s, and Zenscrape at $59 in 2.70s. The most expensive provider was not the fastest. Nothing here says premium providers are bad — it says the number on the pricing page is not a performance figure, and treating it as a proxy for quality is how teams end up paying an order of magnitude for a threshold they could have hit for a rounding error.
The part that makes it actionable is that the relationship is per domain, not global. Price tracked performance reasonably well on Home Depot and Best Buy, and barely at all on Amazon, Walmart, Booking.com, Glassdoor and Google. So there is no ranking to memorise and no provider to recommend in the abstract. The only reliable method is the unglamorous one: benchmark candidates against the sites you actually intend to scrape, at the volume you actually intend to run, and let that decide. It pairs exactly with the success-rate card above — both end at the same place, which is that the only number describing your situation is one you generated.
The diagnostic that saves the week is to notice which layer refused you before choosing what to fix. If the same headers succeed once the transport changes, the headers were never the variable, and every hour spent tuning them was an hour spent proving something that was already true. The tell is exactly what happened here: identical request content, different transport, opposite outcome.
Read this next to the earlier card on the block page naming a different vendor from the header. Both are the same failure of attribution — a confident fix applied to the wrong layer, or the wrong vendor — and between them they probably account for more wasted engineering hours than any technique in this guide accounts for saved ones.
The second-order effect is the one worth sitting with. For a decade this was the trade nobody admitted to at conferences. The AI wave made it a board-level topic and handed legitimacy to an industry that had operated half in the shadows. That is genuinely good for anyone doing it carefully — and it is also why the legislation, the licensing deals and the metering described elsewhere in this feed arrived all at once. Legitimacy and regulation are the same event viewed from two seats.
Worth holding against the guide's own scepticism: the obituary being wrong four times running does not mean it is wrong forever, and “search interest is at an all-time high” is a measure of attention rather than of practitioners. But the mechanism offered here — that models consume the web rather than replacing the need to read it — is the strongest available account of why the prediction keeps failing, and it does not depend on the trend line.
What the blocking actually did. Cloudflare began blocking AI crawlers by default across its share of the web unless a customer allows them, and from 15 September 2026 mixed-purpose crawlers that combine search and training — Googlebot included — are blocked on ad-supported pages unless separated first. The strategy quote is unusually candid: customers use the tools to “create reliable scarcity for their content, then negotiate better deals.” One publisher CEO put the mechanism plainly: they block almost every AI crawler except the one they have a deal with and the one they cannot block, and “they have to pay for it.” Financial Times, The Atlantic, Ziff Davis, Condé Nast and the Associated Press are named as working through the same arrangement. Pay-per-crawl is still in closed beta and the emphasis has already shifted toward pay-per-use, with payment details unsettled.
The waste argument doubles as the pricing argument. More than half the time, by Cloudflare's account, an AI crawler fetches something it has already fetched and which has not changed since. That is the same observation underneath the 402 card — the traffic is expensive and largely redundant, which is precisely what makes metering attractive to the party paying for it.
And the part that leaves engineering entirely. Three US Representatives introduced H.R. 9915, the Stealth Bot Prohibition Act, aimed at bots that disguise their identity to take content. Read alongside the New York statute already in this guide, the direction is consistent: identity disclosure is migrating from a thing you choose to a thing you must do. Whatever happens to this particular bill, a collector whose whole strategy is indistinguishability should notice that the strategy is being legislated at rather than merely defended against. §00 has the case law; this is the other tide.
The check costs one request. Read the response body, not just the status and headers: block pages name their vendor, in visible text or in a script path or a support link. Then look at the cookies actually set on the failure —
_abck and bm_sz mean Akamai, datadome means DataDome, cf_clearance means Cloudflare made the call, reese84 means Incapsula. The cookie jar is harder to misread than the header, because it is written by whichever system actually ran.The general form is a principle already in this guide, applied one level up: the block is at a layer you are not working on, and here it is at a vendor you are not working on. Before choosing a tool, establish which system refused you — and treat the CDN name in the header as the least reliable evidence available, because it is the one thing guaranteed to be there whether or not it was involved in the decision.
USER_TYPE_EMBED_OVER_QUOTA almost immediately — but the interesting part is what happened after the TLS problem was solved.requests and httpx are dropped on their fingerprints regardless of how carefully the headers were assembled. The fix is the one this guide keeps arriving at: curl_cffi with impersonate="chrome", which is a transport change rather than a header change.The second wall is the useful one. Google runs different internal widget endpoints for timeline, heatmap and related-queries data, and their header expectations disagree: the timeline endpoint requires a referer, while sending any referer at all to the regional endpoints triggers a block. Stripping the header completely is what gets the regional data out. Reported result was near-total success with no headless browser anywhere in the path.
Why this generalises past Google Trends. A per-site header profile is the wrong unit. Anti-bot configuration is applied per route by different teams at different times, so two endpoints on one host can hold opposite rules and a profile that satisfies both cannot exist. If a request works on one path and fails on the next with the same client, stop looking for a better global profile and start diffing what you sent to each. The same reasoning is why enumerating routes beats accepting the hardest one.
SG_SS cookie. It is a virtual machine shipped with an encrypted program for it, and the fingerprint is assembled inside that machine.The detail worth carrying into your own work is that the challenge reads “every property, method and deliberately absent name” it cares about. Absence is a signal. A patched environment that adds a property real Chrome lacks, or leaves present something a real build removed, is identified by the gap rather than by the value — which is the coherence principle in its purest form and the reason adding realism keeps making things worse.
On the ethics of the write-up, worth noting because it is a good pattern: the analysis runs offline against a capture you make yourself, so no vendor code is redistributed. That is a reasonable model for publishing this kind of work, and a better one than shipping somebody else's obfuscated bundle in a repository.
The denominator is the part to steal. Cost compared on accepted records and completed refreshes rather than raw requests, because — as the post puts it — that denominator changes many vendor comparisons. It is the same unit this guide's cost section argues for and the same one on the API-versus-scrape scorecard in §01: a request you paid for that produced a record you cannot trust is worse than no request, because somebody downstream acted on it.
Why the reframing matters more than the vendor. Feature lists converge — every managed platform will eventually claim crawl, extract, monitor and an agent endpoint. What does not converge is who is accountable when a source silently changes shape at 3am. Choose on where that accountability sits, then check the price per accepted record, and the feature matrix stops mattering almost entirely.
The lesson is not “ship markdown” or “do not”. It is that the answer differs by nearly three orders of magnitude between clients, so advice treating agent traffic as one population is advice about an average nobody experiences. A site serving mostly ChatGPT users and a site serving mostly coding agents should do different things.
The uncomfortable finding.
/llms.txt was fetched about 660 times and /llms-full.txt about 110 — but only 37 of those came from a named AI assistant. The rest were search crawlers and unidentified bots. And a hidden <link> element pointing at the markdown version, the trick that circulated widely as a cheap win, recorded zero measurable hits across all 268,000 requests.Two operational notes that generalise. AI crawlers do not execute JavaScript, so client-side analytics cannot see any of this — the measurement has to happen in middleware reading raw headers, which is also why most teams have no idea what their agent traffic looks like. And the referrer data was contaminated: 106 of 117 referred hits carried a single frozen
Chrome/111.0 string, a reminder that a user-agent is a claim rather than a fact.Read it against the adoption numbers. Cloudflare's Agent Readiness work, published the same month, scores sites on discoverability, content, bot access control and capabilities, and found 78% of sites have a robots.txt, 4% have declared AI usage preferences in it, and markdown content negotiation passes on 3.9%. MCP Server Cards and API catalogues together appeared on fewer than fifteen sites in the entire dataset. Markdown is worth up to 80% fewer tokens, and Cloudflare's own docs measured 31% fewer tokens and 66% faster to a correct answer than an unoptimised competitor's.
And the dissent worth keeping. One practitioner's answer to the whole agent-readiness push: crawlable is not the same as recommended. A model can index you perfectly and still never say your name, because it also read what the rest of the internet says about you, and that is decided outside your domain. Access is necessary and nowhere near sufficient.
Sources: evilmartians.com chronicles (Rita Klubochkina); blog.cloudflare.com/agent-readiness; isitagentready.com.
The handshake. The edge intercepts the request and returns
402 Payment Required with its terms, the client pays, and the proxy verifies proof of payment before the origin ever executes. No pre-shared API key, no account, no checkout redirect. It is framed as four primitives that only work together — readable (markdown for agents on the server side, Kitesurf as the client-side half), discoverable (agent engine optimisation rather than keyword SEO), callable (WebMCP, where a site exposes its actions as explicit tool contracts with JSON schemas instead of making an agent guess which button to click, plus Code Mode where the agent calls endpoints in code rather than prose), and payable (x402 and PACT tokens, with wallets on the agent side).Why a collector should care, in both directions. If you gather data, this is the first credible pricing mechanism aimed squarely at you, and it prices per request rather than per contract — a very different negotiation from a rate card. If you run a site, it turns the bot question from block-or-allow into a third option, and the argument for it is commercial rather than moral: behind almost every agent is a paying customer, so treating agents like legacy scrapers means losing the customer.
The honest caveat. This is largely one vendor's stack described in that vendor's own posts, and the adoption numbers in the card above — fewer than fifteen sites in the dataset carrying an MCP Server Card — say the callable and payable layers are proposals rather than conditions. Worth understanding now, worth building against later, worth not rearchitecting around yet.
The pressure point they name is faceted search, and it is the most useful thing here for someone on the other side. Every filter, category, sort order and pagination state generates its own URL, so a crawler walking filter combinations produces a combinatorial explosion of uncached requests, each one forcing PHP execution and database queries. That is why a site which serves you happily on product pages turns hostile the moment you start walking facets: you moved from layer three to layer four, and you did it at volume.
Two things to take from it. First, the defensive posture you meet is a function of which layer your traffic lands on, not of how the site feels about scraping in the abstract — requesting cacheable URLs at a sane rate is genuinely cheap for them and is therefore tolerated far longer. Second, they put automated traffic at more than half of all traffic for a typical site, which is the number that funds the entire defence industry described in §03.
Source: amazee.io, layered Drupal bot and scraper defence.
akamai_solve (for _abck), datadome_solve (from an HTML response), plus health_check, solver_stats and akamai_queue_metrics for backpressure. The usage pattern in the demo is a prompt rather than a script: fetch this URL, and if you get a 403 or an access-denied page, use the solver, then retry the same request with the cookies it hands back.The technical point worth extracting is why the Akamai path drives a real Chrome relaying each sensor request over the session socket instead of computing the sensor server-side. Solving server-side transmits the container's TLS fingerprint rather than a browser's, and any challenge that verifies the client actually submitting it will reject the answer: correct sensor payload, wrong envelope. A run against a live commercial target completed in 20.8 seconds.
The maintenance lesson underneath it applies whether or not you ever touch this. The browser identity had been fragmented across several files, so the Akamai scripts had quietly drifted two versions behind the DataDome clients. The fix was a single profile module everything derives from — DataDome clients, Akamai telemetry, CDP overrides, MITM headers, the MCP tools — with values like
sec-ch-ua computed rather than hardcoded. A fingerprint assembled in more than one place will eventually disagree with itself, and that disagreement is what gets caught.One operational constraint stated plainly and worth repeating: the clearance cookies are bound to the exit IP, so the retry has to leave through the same proxy. An agent whose built-in fetch tool egresses somewhere else will earn a fresh 403 while holding a perfectly valid cookie.
The deployment model is the actual differentiator, and it is worth separating from the MCP novelty. This ships as a Docker image you run on your own hosts: your worker calls the container on localhost, the solver generates the payload, and a valid token comes back without a single external API call. That is the opposite shape to every managed unblocker in §07 — those are per-request calls to somebody else’s endpoint, which means your target list, your URLs and your timing all leave your network. Here nothing does, which makes air-gapped deployment possible. Billing follows the architecture: a flat fee for unlimited solves rather than per-request or per-GB, which inverts the crossover maths in §13 — the cost stops scaling with volume and starts being a fixed line.
One claim worth reading against the artifact. The product page describes solving at the payload level, “milliseconds, not seconds”, with no Playwright instances and no per-challenge browser overhead. The pull request above, which is the changeset that adds the Akamai path, drives a real Chrome and reports 20.8 seconds. Both can be true at once, and the reconciliation is the lesson: the payload-level story holds for DataDome and does not survive contact with a vendor that verifies the client actually submitting the answer. Read any blanket “no browsers needed” claim per vendor, not per product — and note that the honest version of the architecture is visible in the repository while the simpler version is on the homepage. The commit is the better source, which is generally true and is why this guide keeps linking them.
Source: xhrdev/examples PR #40.
Idempotency-Key header, no unique constraint, and an ingest endpoint calling create() keyed on nothing: same URL five times, five items.Their own framing of the mistake is the line worth keeping. They had built the identifier story and mistaken it for the write story. A UUID makes a row addressable once it exists. It does nothing about the request that created it arriving twice.
Why it lands harder with an agent on the other end. A human sees the duplicate and files a bug. An agent times out, cannot determine whether the write landed, retries, and says nothing — so the duplicate arrives silently and surfaces much later as a data-quality problem with no obvious cause. That is the same shape as the silent-200 this guide keeps returning to: invisible where it happens, expensive where it shows up.
The two fixes named. Either a client-supplied
Idempotency-Key stored behind a database constraint, replaying the stored response on a repeat; or, for a narrow ingest endpoint, get_or_create on the natural key such as (owner, url). If you are building the API your own collection pipeline writes into, this is a ten-minute check: send the same request twice and look at what you get.Source: bessavagner.com, an API-first vault my agents can call.
Answer two, keep the real browser and make it cheap to run thousands of. Unikraft's founder puts the objection to answer one bluntly: you cannot run JavaScript, render a page, take a screenshot or click a button from a comfortable CLI, and agents touching real sites need a genuine engine underneath rather than a lightweight stand-in. His framing is the useful part: the problem was never whether agents need real browsers, it is what happens once you try to run thousands of them. That reframes it from a rendering problem to a cold-start and isolation problem, which is unikernel territory rather than browser-engine territory.
Answer three, put the operating system in the browser instead. BrowserPod 3.0 goes the other way: rather than shrinking the browser, it implements Linux syscalls in WebAssembly — a custom
wasm32-browserpod-linux-musl target rather than WASI, giving a persistent virtual filesystem, inbound and outbound networking, threads on separate Web Workers, and real subprocesses. They compiled and ran ripgrep, Starship, jj and OpenAI's Codex unmodified, the last at around 1.25 million lines. The limitation they state plainly is that Rust programs must be compiled offline, and the target declares arch="wasm64" while excluding the wasm family tag to dodge ecosystem assumptions about reduced-capability builds.And the dissent, which cuts all three. A sceptic's post on running full browsers in V8 isolates called it a leaky abstraction and asked the question the whole category should answer: for production agents you usually need specific data points, not a simulated DOM and rendering engine, so a purpose-built HTTP client with proper error handling and retry logic will beat a simulated browser most of the time. He is right about the median case, and this guide's own escalation ladder agrees with him — plain HTTP first, browser only when proven necessary. Where he is too strong is the tail: login flows, canvas-rendered content and JavaScript-constructed data are not reachable from a CLI at any level of retry sophistication. The honest position is that all three architectures are answers to the minority of traffic that genuinely needs a browser, and the argument is only about how expensive that minority has to be.
The ordered procedure, worth giving to a model verbatim: start with plain HTTP; check for the fields and the minimum record count you asked for; move to a browser-like client only when the response proves a transport mismatch; launch a full browser only when JavaScript is what creates the data; and stop on login, CAPTCHA, explicit denial or unclear permission.
The formulation that makes it work is the acceptance criterion itself: "twenty records, four required fields, same schema on a second run" gives a model something it can actually prove. "The request worked" does not. That single sentence fixes the model-authored-oracle problem described in the agent-built-scrapers section above, because it replaces a check the model can satisfy by agreeing with itself with one it has to demonstrate against reality — and the "second run" clause quietly catches the case where a page happened to render once.
A related habit worth stealing, from a third post. An agent hit a third-party upload form stuck on "Uploading…". Rather than retrying a fourth time, it opened the browser's network trace: the request for a presigned upload URL returned 200, and the follow-up PUT to storage never fired — a client-side bug in the site's own widget, not a network or server fault. It then grepped the captured requests across all three attempts to confirm the same pattern, and afterwards verified the outcome on the site's own confirmation page rather than trusting the optimistic "done" state in the UI. Read the trace before you retry, and verify against the source of truth rather than the interface is ordinary engineering discipline; the notable part is that it is now something you can expect from an agent mid-task, and something worth explicitly instructing when you do not get it.
One boundary from the same author, useful if you are choosing where to put this logic: a skill teaches a model how to build and verify a scraper; an MCP server is for when you need it to call a running tool on demand. They are not competing, and reaching for the second when you needed the first is a common and expensive mix-up.
Why that gap matters more than the technique. Bounty programmes encode what a vendor thinks it can be hurt by, and the hierarchy runs roughly: remote code execution, then sandbox escape, then everything else. A privacy vulnerability produces no crash, no compromised device and no incident report, so it lands in "everything else". The predictable consequence is that the people best placed to find these have a commercial reason not to file them, and the ones who do file get a sticker. He also makes the point that follows: AI is making bug-finding dramatically cheaper, so the volume of findable issues is rising while the incentive to disclose this particular class stays flat.
The practical reading for anyone on either side of detection. Assume a meaningful backlog of unreported, hardware-adjacent, cross-site identification techniques already exists inside companies whose business model rewards holding them. If you are scraping, that is another argument for treating "fresh profile" as a claim rather than a fact, and for putting real machines behind sensitive work. If you are defending, it is a reason to be sceptical of a vendor's originality claims: this capability class is more widely held than published research suggests, because publishing is the least profitable thing you can do with it. He notes separately that the bot-detection industry has one of the highest talent churn rates he has seen, sometimes weeks, which is part of why vendor behaviour is less coherent than the marketing implies.
<ai_context> instructions in the repo, installs the Playwright binaries, and configures the tooling for itself.llms.txt, WebMCP tool declarations, accessibility-first markup, and now repositories carrying embedded setup instructions addressed to whatever model is reading them. The through-line is that the machine consumer has stopped being an afterthought parsing documentation written for people, and is being handed its own channel.Which is genuinely useful and also a small, clear security lesson. A repository that instructs your agent is, definitionally, untrusted text that your agent will act on. It is the supply-chain version of the prompt-injection problem in the agentic section above: nobody had to compromise a package registry, because the instructions are the feature. An
<ai_context> block that installs binaries and rewrites your tool configuration is doing precisely what an injection payload would do, with your consent, and the only difference between the helpful case and the hostile one is the author's intent, which you cannot read from the file.So treat it as you would any install script. Read the embedded instructions yourself before pointing an agent at a repository, particularly one that installs browser binaries or edits configuration. And note the asymmetry that makes this worth flagging rather than shrugging at: a human skimming a README will not see an
<ai_context> block at all, while the agent reads it as the primary content. That is the same invisible-to-humans, authoritative-to-machines shape as the canvas injection vector and the markdown edition a publisher serves only to bots.Why this belongs next to Frost and the WASM SIMD probes rather than with cookies. Every mitigation you have for tracking assumes the state lives somewhere the browser namespaces per origin and clears on request — cookies, localStorage, IndexedDB, the HTTP cache. This is a shared compilation artefact of the graphics stack. It is not partitioned by origin in the way storage is, which is what "accessible from literally every site" means, and there is no user-facing control pointed at it. The fix has to come from browser vendors partitioning or keying the cache differently, and that carries a real performance cost they will have to weigh, exactly as with disk-timing mitigation.
For a scraper, invert it before you dismiss it as a privacy story. A persistent, cross-site, hard-to-clear identifier attached to a machine is precisely what a bot-management vendor wants most, because it survives the thing you rely on: a fresh profile. Everything in the fingerprint-replay economy assumes that a clean browser is a clean slate. If a hardware-adjacent cache carries an ID across your profile resets, then "new session" stops being true at the layer you cannot patch, and the answer is the same one that defeats SIMD probes and math fingerprints — separate real machines, not separate profiles on one.
Worth pairing with a gap another practitioner named the same week: we have plenty of tools that patch a browser, and almost none that let you watch what an anti-bot script is actually reading from your system. Techniques like this arrive faster than the instrumentation to detect them being used. shaderghost.gg
The weakness is that a tracker gets to choose what it looks at. It can simply ignore the fields it knows are randomised and identify you on whatever stayed stable. Randomising the canvas hash is worthless if the identifying signal was the math library, the compilation cache, the TLS stack or the timing profile, none of which your anti-detect layer touched. Which is the same conclusion the guide reaches from the other direction: the coherent-story problem is not solved by making more fields noisy, because noise in a field the detector has written off is free for them and expensive for you.
The sharpest line is methodological, and it generalises well beyond fingerprinting. Preventing identification and proving you have prevented identification are different problems, and the second has received remarkably little attention. Applied to your own stack, that is a request for a specific kind of evidence: not "my fingerprint changed between runs", which any tool can show you, but "sessions that should be unlinkable were not linked by the target", which almost nobody measures. If your only evidence that rotation works is that a test page shows different values, you have measured your tool rather than the detector. The fingerprint dilemma
localhost, and a script on an ordinary web page in the mobile browser can reach it. Neither platform's model is broken on its own terms. The browser is doing what it does with a local address, and the app is doing what any app may do with a socket. Between them, the browser's per-origin isolation is bypassed entirely, and a web session can be joined to a device-level app identity with no permission prompt and nothing visible to the user — which is exactly the linkage both platforms are supposed to prevent.The outcome is the part worth noting, because security research often stops at the finding. This one produced concrete change: browser mitigations, a new Android permission, and regulatory attention. The authors are explicit that the changes mattered more than the awards.
Two reasons it belongs in a scraping guide. First, it is the cleanest recent example of a class this guide keeps returning to: the highest-value signals are not in any one layer's threat model but in the seam between two layers, where each component is behaving correctly and the combination leaks. TLS versus user agent, JA4 versus the JavaScript that runs after it, an app's signing logic versus the OS it trusts — same shape. Second, if you do mobile work, a localhost listener is now a permissioned, mitigated surface rather than a free one, so anything you built that leaned on app-to-web bridging should be re-tested rather than assumed. USENIX Security 2026
Read it alongside the RAG acquisition-layer argument above and the tool-surface economics in the agentic section, and it is the same claim arriving from three directions: the expensive failure in 2026 is rarely the fetch. It is boilerplate competing for similarity with your actual content, payload you pay to re-read, and corpora nobody can audit.
One extension worth thinking about, from Nimble's founder. Systems are starting to accumulate knowledge about what they found. The more interesting question is whether they can accumulate knowledge about how to find it — better source selection, better retrieval paths, a sharper sense of where the relevant information for a given domain tends to live. Those are two different kinds of memory and both compound. It is the same instinct as keying scraper memory to the platform rather than the URL: the durable asset is not the record you extracted, it is the route you learned to it.
The second half is the part almost nobody writes about, and it is the more useful lesson. The tool being used to check whether a proxy was alive did not look like a browser either, so it got blocked too, always. That means the instrument was reporting healthy proxies as dead and actively confirming the wrong theory. Weeks of evidence, all of it generated by a broken measurement. If your health check is less capable than your scraper, every reading it gives you is noise dressed as data, and you will keep buying the conclusion it hands you.
The fix is worth copying as a pattern, not just a value. Rather than finding the correct setting, they made the wrong one impossible to express: the handshake profile and the user agent now travel together as a single unit, so you cannot select one without the other. The bug cannot be recreated by a future edit. That is the difference between fixing a defect and removing a class of defect, and it is the same instinct as binding a fingerprint's axes together rather than configuring them independently. Result: roughly 0% to 90% success, with exactly one block across the following 50,000 requests.
/searches, the primary listing endpoint, and /map-listings, which returned largely the same property data plus some additional fields. Only /searches carried the protection. The whole architecture collapsed from browser, then cookies, then tokens, then signatures, then protected API down to direct API, structured JSON, database. No browser automation, no cookie management, no session persistence, no token extraction, and it ran continuously afterwards.Why this generalises further than it looks. Anti-bot is usually deployed as a rule on a route, not a property of a domain, and the rules are written by people under deadline. Map views, autocomplete, mobile endpoints, embed widgets, sitemap-adjacent JSON, RSS and print views all commonly serve the same underlying records through paths nobody thought to cover. The map endpoint in particular tends to be richer, because a map needs coordinates and metadata a list view does not.
So make endpoint enumeration a step, not an afterthought. Before you accept that a target needs a full browser stack, read the bundles and list every route the frontend can call, then test each one cold with a plain HTTP client. This costs an hour and regularly turns a Kasada or Akamai problem into a JSON problem. It is also the cheapest thing in the escalation playbook above, which is exactly why it should come before you reach for a browser.
Then the same three went against a live Cloudflare Bot Management deployment rather than a JS-challenge demo. All three had cleared the demo cleanly, rendering real content with no challenge page. All three hard-403'd on the real target, at an identical rate, regardless of which had the cleaner fingerprint an hour earlier. Attempting to fix it with a rotating proxy pool produced six attempts, four hard blocks, one dropped connection, and one that served real page content under a 403 status anyway. The sampled exit IPs were still datacenter and cloud infrastructure rather than residential, which is the actual reason nothing changed.
Three things this pins down that the guide has argued and can now point at. First, a public detector measures whether you pass a public detector; it is not a proxy for a tuned commercial deployment, and the gap between the two is where most stealth-tool comparisons quietly live. Second, once the network layer is wrong, fingerprint quality stops being the variable, which is why the engine choice made no difference at all here. Third, and the part they said they did not expect to relearn so cleanly: maintaining fifteen separate fingerprint-spoofing engines is real, recurring engineering cost buying a difference that a datacenter IP erases completely. Fix the layer the block is happening on before you invest in the layer you enjoy working on.
WebMCP widens the same wound, and this is the part teams miss when they ship it. Every tool you register is effectively a public API that a stranger can invoke with words you never wrote. An attacker needs no login and no stolen token; they plant text where the agent will read it, and the agent calls your tools using its own trusted session — the user's cookies, the user's CSRF token, the user's permissions. A registered
updateEmail or applyCoupon executes as the logged-in user. Chrome's own guidance is explicit that registered tools are an attack surface and prompt injection is the delivery mechanism.What follows for both sides. If you publish tools, decide before launch which actions an agent may take unsupervised and which require a human in the loop, because that is a design decision and not something you bolt on afterwards. If you run agents against the open web, the question to sit with is the one Chaikin's work implies: would you let an agent browse arbitrary pages using the same account it holds production access with? The guide's own prompt-injection material covers the URL-fragment surface that never crosses the network; this is the same threat with a session and a tool list attached.
On a 192-URL crawl it reported a success rate level with Chrome, 53.6% against 52.6%, at a tenth of the memory. It speaks CDP from a single binary, so existing Playwright and Puppeteer scripts connect directly.
Read the caveats as stated, because they matter. The benchmarks are self-reported, there is no Chrome pixel parity, and the project is young. Note also what that success rate is telling you: roughly half of a 192-URL crawl failed for both engines, which is a reminder that the engine was never the thing deciding whether you get in.
This lands squarely in the lighter-browser thread alongside Kitesurf, Obscura and Lightpanda, and the convergence is now hard to miss. Kitesurf is Blitz rendering plus Firefox's Stylo plus the Boa JS engine, built in twelve weeks on Workers, and Cloudflare credits Obscura as the inspiration. Moli uses V8 and Stylo. Everyone building for machine consumers is arriving at the same conclusion independently: keep the JavaScript engine and the DOM, throw away the rendering pipeline, and keep CDP so nobody has to rewrite their scripts.
1. Warm up before you deep-link. Navigating straight to page two of a protected listing gets you blocked, and the symptom is misleading: an HTTP/2 protocol error, which reads like a network fault and is actually the anti-bot refusing you at the connection. The fix is to load the homepage first so the session collects
_abck and bm_sz, then navigate to the target within the same session. Same lesson as the session-continuity material above: the cookie is not decoration, it is the thing that makes the next request legible.2. Listen, do not scrape. The capture is a
page.on("response") handler that filters on content type (application/json and the +json variants), drops the analytics noise by host (Google, Facebook, OneTrust), deduplicates by HTTP method plus base path so you get one row per endpoint rather than one per request, and writes the bodies to disk for reading afterwards. Twenty lines, and it is the difference between finding the endpoint and guessing at selectors.3. When a click does nothing, click in the DOM. Client-side pagination often ignores a normal automation click.
page.eval_on_selector(sel, "el => el.click()") dispatches it from inside the page and gets the handler to fire.4. The credential you were about to go hunting for may not exist. In the worked example the internal API turned out to be a proxied Algolia endpoint served from the retailer's own domain, with the credentials injected server-side. There was no key to extract, because the site was authenticating on your behalf. Always check whether the endpoint is first-party plumbing in front of a third-party search service before you start reverse-engineering a token.
Read this against the agent-built-scrapers section above. The single largest finding there was a toolchain that launched a real browser and registered no response listener at all, keeping only
page.content(). This is the twenty lines it was missing, and the reason that omission decides the whole strategy. The Lab #112, The Web Scraping ClubFreshness. A static knowledge base decays. Index six months ago and you have rebuilt the exact knowledge-cutoff problem RAG existed to solve, only now you own the infrastructure too.
Coverage. Search engines index a fraction of the public web. Recently published pages, low-authority domains, anything dynamically rendered, and anything behind an anti-bot are invisible to search-backed retrieval. If your pipeline gets its documents through a search API, your corpus has the shape of someone else's index, including its blind spots.
Extraction fidelity. Feeding raw HTML in means navigation, footers and cookie banners enter the corpus as content, and in the author's phrasing that is noise no chunking strategy can fully recover from. Boilerplate does not just waste tokens, it competes for similarity with the text you wanted.
Chunking is downstream of all of it. Poor source text produces semantically incoherent chunks, which lowers retrieval precision, which looks like a model problem and is not.
The practices that follow. Tiered crawl schedules matched to how fast each source actually changes, rather than one global interval, so breaking news is hourly and reference pages are weekly. Version-controlled vector stores, so a retrieval result is reproducible and auditable months later. Extract to structured JSON or Markdown, never raw HTML. Treat freshness as an SLA you can be held to rather than a vague intention. And use purpose-built scrapers for the sources that matter to you instead of outsourcing coverage to a search integration.
This is the same economics as the cost-per-usable-document material in the cost section, arriving from the AI side: the expensive failure is never the fetch, it is discovering months later that what you collected was incomplete, stale or full of furniture. Full piece
UDP ASSOCIATE command, but browsers do not expose it, so Chrome is TCP-only over SOCKS5 in practice. There is no combination of a stock browser and a conventional proxy that produces an HTTP/3 request. Your fingerprint work at layers 1 and 2 can be flawless and you will still be announcing something before a single byte of TLS is compared.What makes it worth acting on is the trend, not the level. Cloudflare Radar's likely-bot share by protocol version: HTTP/1.1 at 73.75%, down from 82.07% a year earlier. HTTP/2 at 26.76%, up from around 21%. HTTP/3 at 3.33%, essentially unchanged. Read those three numbers together and the migration is visible: automated traffic is moving off HTTP/1.1 into HTTP/2, and almost none of it is arriving over HTTP/3. If that continues, protocol version stops being a weak prior and becomes a strong one, and the proxied browser is structurally parked on the wrong side of it.
The honest options, none of them free. RFC 9298 defines UDP proxying over HTTP (CONNECT-UDP), which is the standards-track answer and needs your provider to implement it. A network-layer tunnel (a VPN rather than an HTTP proxy) carries UDP and therefore carries QUIC, at the cost of the per-request IP control that made proxies useful. Or wait for browsers to expose UDP proxying, which nobody should plan around. A small number of providers have started advertising HTTP/3 support; that claim is now worth testing rather than assuming, and it is easy to test, because the negotiated protocol is visible in your own response. Full writeup by Web Scraper
Google fixed the underlying bug in September 2025, years after it was first reported. Once the fix is broadly deployed, the check is dead.
Three things worth taking from this. First, the highest-precision detection in a vendor's arsenal is often not a model, it is a discrepancy, and discrepancies get patched by someone with no stake in the detection. Second, this is the same story as browser vendors quietly softening
navigator.webdriver and CDP detection: the detection surface is owned by the browser vendor, not the anti-bot vendor, and the browser vendor's incentives now include making agentic browsing work. Third, and most practically, the class of bypass that survived this was input generated outside the browser — xdotool, AutoHotkey, pyautogui — because a click synthesised at the OS level is not a CDP click at all, it is a real one, and it carries real coordinates. That is the general shape of the answer whenever a detection keys on how an action was dispatched rather than what it looked like. The teardownThis is now the fifth independent measurement pointing the same way, and at this point it is a settled finding rather than a claim. The Browser Use team found strong models rarely want screenshots and prefer to inspect the page through code they write themselves. The accessibility-tree work found the win comes from operating known UI patterns as atomic actions with no model round trip at all. Hermes swapped twelve per-action browser tools for a single script-driven engine on browser-use's CLI 3.0, and Nous Research measured 48 to 66% fewer tokens with no drop in accuracy — because a dozen tool schemas were riding along in every request whether used or not, and opening a page to read one headline cost four or five round trips. Vercel's agent-browser made the same move from MCP tool schemas to a lightweight CLI and reported 3.5× faster execution and 37% fewer tokens. And this one finds most of the bill is payload and tool sprawl rather than reasoning. Five teams, five methods, one conclusion: the expensive thing in agentic browsing is the number of times the model has to look and the number of schemas it carries while looking, and the cheapest optimisation is to make it look less often rather than to make it think better. The shape of the fix is identical everywhere too — stop exposing one tool per action, and let the agent express a whole task as one script.
If you are building on an agent framework, this is a config-level change rather than a rewrite, which makes it one of the few genuinely cheap wins in this part of the stack. Measure your own tasks though: these gains come from removing waste specific to a tool surface, so how much you recover depends on how chatty yours is.
Five tools, placed by layer rather than by popularity. curl_cffi impersonates browser TLS and HTTP/2 fingerprints from Python and runs no JavaScript, so JS challenges are exactly where it stops — and it is the cheapest thing to try, so try it first when you do not need page JavaScript. fingerprint-suite is Apify's Node toolkit: one call gives you a Playwright context where headers and JavaScript APIs are generated from a single coherent fingerprint, which is the whole game (see the coherence material above — the failure mode is spoofing one axis and leaving the others pinned). Obscura is an independent Rust and V8 engine with no Chromium underneath, currently v0.2.0, and its own notes warn that rendering and Web API behaviour can diverge from Chromium — the same structural trade as Kitesurf. TRAWL is the escalation ladder as a product: plain HTTP first, then a cached session, then a fresh browser solve, then a residential proxy, so a target that yields at rung one never costs you rung four — roughly sub-100ms, ~500ms, seconds, then tens of seconds, and it speaks FlareSolverr v2 so it drops into existing stacks. stealth-browser-mcp exposes a stealth browser over MCP, pinned to nodriver 0.47.0 — useful, and a good illustration of the last point.
The closing advice is the part people skip: check the recent commits before you install any of them. In a field where the detectors ship weekly, a stealth tool's commit history is a freshness indicator, and a pinned dependency means you inherit that driver's fixes only when someone remembers to bump the pin. Read the pin as a date, not a version.
role=combobox with accessible name "Fare", then selectOption("Economy"). One step, zero round trips. Learned skills: roles and accessible names stay stable across sessions in a way generated selectors do not, so a successful run can be cached against them. A ten-step flight search on united.com collapses into find_flights(origin, destination, dates) and later runs just fill in the parameters.Against Browser-Use on the Online-Mind2Web benchmark: 5.4× faster and 3.2× cheaper in tokens on average. The combined price-performance number is the one worth sitting with — roughly 32×, not 17×, because the two gains are positively correlated: the flows where you cut the most tokens are the flows where you cut the most time, since both come from deleting the same round trips. The distribution is heavily right-skewed, with about 11% of flows over 100×. Sites that score well on accessibility checks gave much bigger gains.
Why this belongs in a scraping guide rather than an accessibility one. The thing that makes agent automation expensive is not the browser, it is the model call between every action, and the accessibility layer is a pre-existing, standardised, semantically-labelled index of the page that most scrapers ignore in favour of selectors they have to repair. If you run agent-driven extraction at any volume, the role/accessible-name pair is a selector strategy with a maintenance profile better than CSS and a cost profile better than vision. The honest caveat, from the authors: what an agent needs from a page is a subset of what a person needs, so agent-readiness does not mean a site is actually accessible. Full writeup in The AI Journal
X-BRD-ERR-CODE header and now also supports RFC 9209 Proxy-Status, the IETF standard designed for exactly this. Oxylabs adds an X-Error-Description header only when the fault is on their side. Decodo documents dedicated error codes in its response headers. Massive went further and, from 17 August, returns dedicated HTTP status codes (452–454 and 470–477) that are unambiguously theirs, plus an X-Massive-Reason header with the exact cause — no header parsing, the status code alone tells you policy, infrastructure, or target.The disagreement is the useful part. The immediate pushback, from practitioners in the thread, was that proprietary status codes are the wrong abstraction: this is precisely what
Proxy-Status was standardised to solve, and having to learn that 452 means one thing at one provider and 472 means something else at another is the fragmentation the RFC exists to prevent. The counter-argument is equally real: a status code needs no parsing and no client library, so it gets adopted, and an RFC that nobody implements attributes nothing.What to do while it settles. Do not wait for a winner. Add one field to your request log now — call it
failure_owner, values proxy_policy / proxy_infra / target / unknown — and populate it from whatever your provider gives you today, header or status. The value is not in the encoding, it is in being able to answer "what share of last week's failures were ours" without re-running anything. It also changes your provider conversations, because "unknown" trending upward is a contract question, not an engineering one.llms.txt, not via a .md version.The consequence for anyone who wants to be read by an assistant. The search index becomes the gatekeeper rather than your link graph. For Claude that means Brave Search is the practical entry point — a page that is not indexed there may simply never be reachable, no matter how clean your internal linking is. Absolute URLs help, because an absolute URL that appears in fetched content has provenance where a relative one does not. Practitioners in the thread noted the same asymmetry applies to every assistant with its own index, and that there is no Search Console equivalent for most of them, so the only honest method is to probe and measure rather than assume.
The mirror image, for scrapers: this is a reminder that agent traffic is shaped by tool policy, not just by crawl budget. If you are modelling which of your pages assistants can see, the question is not "did I allow the bot" but "did the URL ever have provenance". Anthropic's web-fetch tool documentation
document.modelContext, the browser API shipping experimentally in Chrome 146, so browser agents can discover tools on your page. The plumbing is real. The part people assumed was included is not — the pack that lets an agent take a real action is a proxy that forwards to an MCP server you are already hosting at your own endpoint.The subtler critique came from the Chrome side, and it is the one worth keeping. Treating WebMCP purely as a tunnel to an existing MCP server throws away the thing that makes it different: WebMCP tools are part of your frontend code, with access to client-side state — cookies, local storage, the DOM — and can write to it as well as read it. A
set_filters tool can actually change what the user sees. And registering the same tools twice, once via an MCP server and once via WebMCP, duplicates them for the agent, inflating tool count and creating genuine ambiguity about which to call.What this means if you scrape. A growing number of sites will expose a declared, structured, versioned tool surface next to their HTML — and the two can disagree, because they are maintained by different code. That is the same lesson as the publisher-serving-bots-a-different-site card, arriving from the opposite direction: the page a machine is offered is increasingly not the page a person is offered. Check for a tool surface before you write a selector, and if you find one, diff what it returns against what the rendered page shows at least once, because you are now choosing between two sources of truth rather than parsing the only one. Cloudflare's WebMCP announcement
The inversion is worth stating plainly, because it is genuinely counter-intuitive against everything else in this guide: the authenticated-looking session was rate-limited while the anonymous one was not. Both behaviours are rational from the defender's side. Anonymous traffic gets a generic bucket. Identified traffic gets a per-identity budget, and a per-identity budget is a thing you can exhaust.
How to use it without over-learning it. The lesson is not "never send cookies" — on a site that scores session continuity, dropping cookies is exactly how you get flagged. The lesson is that headers, cookies, tokens and fingerprints are all claims, and each claim is something that can be held against you as well as for you. So before scaling infrastructure to get around a limit, run the cheapest possible experiment: the same request with the state removed. If the anonymous request succeeds where the identified one fails, you were not being rate-limited by IP, you were telling the server exactly who to rate-limit. Add it to the block-triage checklist alongside "try a different TLS profile before you escalate the tool" — both are one-request experiments that save a week of the wrong work.
RST_STREAM, repeat — 398 million requests per second from about 20,000 machines, and every frame was valid HTTP/2 using a feature real browsers use. Mitigated by counting resets per connection and capping concurrent streams. CONTINUATION Flood (2024): unbounded HTTP/2 CONTINUATION frames without END_HEADERS, so the server buffers until it dies — and because the request never completes, it is never written to the access log. The server falls over with no evidence.Why a scraper should care. Every one of these forced defenders to move from cheap packet-level filters to expensive behavioural comparison against a baseline — CPU the attacker never had to spend. That is the same machinery that decides whether your crawler gets through, and it explains something that confuses people: why a modest, well-behaved scraper sometimes gets throttled harder than a loud one. Loud is cheap to classify. Ambiguous is expensive, and expensive traffic gets treated conservatively. It also explains protocol-level limits that look arbitrary — concurrent-stream caps, reset accounting, header-size ceilings. If your HTTP/2 client aggressively cancels requests, reuses connections unusually, or sends unusual header volumes, you are wearing a costume with a known bad reputation, entirely by accident. bunny.net's full breakdown
The trade to be honest about. This solves the blocking problem for authorised, logged-in, user-owned work, and it is the correct architecture for that case — it is the same argument the Computer Use Agents section makes about data behind a login the user owns. It does not solve public-web collection at volume: one machine, one residential IP, one browser profile, and the moment you need geographic distribution or concurrency beyond a laptop you are back in proxy territory. Read it as a category boundary rather than a replacement. browseros-ai/BrowserOS on GitHub
The market context around it, mid-2026. A legal win, then a second round: Google's DMCA claim against SerpApi was dismissed on 20 July 2026 in the Northern District of California, with the claims resting on search results containing no copyrighted content thrown out for good, on the reasoning that the DMCA does not protect material that is not copyrighted. The court left Google 21 days to amend on a narrower set, the results carrying a copyrighted component such as the snippets in Knowledge Panels. Google refiled on the final day, 10 August 2026: a 15-page amended complaint built around the one element the court found missing, written permission from copyright owners, and naming Reddit as the partner that asked it to block scrapers. The question has therefore moved from whether scraping public pages is unlawful to whether the platform holds licences that make the content protectable in the first place, which is a licensing-chain argument rather than an access one. A vote of confidence: Oxylabs took a $130M investment at a reported $3.6B valuation. And two warnings that the residential market's Wild West era is closing — the FBI seized domains connected to NetNut and its sourcing network amid fraud allegations, and LG told developers to strip residential-proxy functionality from smart-TV apps or face suspension. Expect more KYC, tighter reseller controls, and far more scrutiny of where residential IPs come from.
The third finding is the one that ties back to this guide's detection section. A perfect browser fingerprint is no longer sufficient: anti-bot systems are moving from checking individual fingerprint values to judging the whole session. You can pass every discrete check and still fail because the browser, OS, hardware, network, navigation and behaviour do not tell a coherent story together. Which is the same conclusion the guide reaches from the other end — you do not get blocked for looking like a bot, you get blocked for not making sense. There is also a practical corollary nobody says out loud: this environment rewards continuous measurement over one-off research, because a stack that was correct three months ago is a stack you have not tested.
Their argument is that the answer is not a better filter but a better unit of value — a known audience, because a bot can fake an impression but cannot validate a work email, complete a profile and come back three more times. Whether or not you buy the framing, the underlying observation is the one that matters for anyone building collection infrastructure: the identity of the requester and the identity of the beneficiary have come apart, and every detection system still assumes they are the same.
The legislative counter-current is moving the other way. "Stealth crawler" has gone from slang to a term in draft law: a bill passed in New York last month and legislation introduced in the US House in July both aim to prohibit bots from masking their identity. That is a live risk to a large amount of current practice, and the honest read is that the disclosure question — who you say you are, in your user agent and in your headers — is drifting from an engineering choice toward a compliance one. Track it in the legal section of this guide, and note the direction of travel: identification is becoming cheaper to comply with and more expensive to avoid.
User-Agent, and compare. As Chrome or Safari the response is text/html, 303,235 bytes, and Google's search crawler receives the same page a person does. As ClaudeBot, PerplexityBot or OAI-SearchBot the response is text/markdown, 13,409 bytes, roughly one twenty-third the size, byte for byte identical between those three. GPTBot and ChatGPT-User are refused outright with a 406. Inside the markdown, where no person would ever encounter it, sits sponsored content: adtech vendor Mobian serving FAQ blocks for an online bank and a professional body, with tracking links carrying campaign identifiers, and response headers
x-mobian-impression (a fresh UUID per request) and x-mobian-tokens counting what was served. Three separate audiences, three commercial treatments, decided at the header.Why this matters to a scraper, and it is not the obvious reason. The guide has covered poisoned data, where a site detects you and quietly serves wrong values. This is the opposite in intent and identical in consequence: the document you receive is legitimately, deliberately not the document a human receives, and nothing about it looks like a block. Your status code is 200, your parse succeeds, your record count is plausible, and you have collected a different edition of the page. Two habits follow. Diff your extraction against a browser-rendered fetch of the same URL, not just against yesterday's run, because a content-type shift from html to markdown is invisible to a selector-based validator that only counts fields. And treat the identity you declare as a variable that changes what you are served, not merely whether you are served. Vincent Schmalbach's original teardown
The limitation is the interesting part, and Cloudflare states it plainly. Kitesurf does not support bot-challenge handshakes that require real TLS fingerprints, and it has no WebGL, no video, and no long authenticated sessions that need persistent state. For those cases the documentation points you back to Chromium. Read that against Layer 1 of the detection section and the reason is structural rather than a missing feature: a browser that is not Chromium cannot produce a Chromium ClientHello, and a stateless engine spun up per request has nowhere to accumulate the session trust that vendors like Akamai score across multiple requests.
This lands squarely in the lighter-browser thread alongside Obscura and Lightpanda: when the consumer is a model rather than a person, most of the rendering stack is dead weight, so the sensible move is to drop it. Kitesurf is the largest infrastructure provider on the web making that bet in public. It also puts Cloudflare on both sides of the same street, selling the bot management that decides which automated clients get through and the browser those clients drive. Cloudflare Browser Run docs
The finding that matters most is the one that reads as good news. Despite all of that, 88% of landing pages still sit in the two lowest accessibility tiers. Both halves of that are true at once, and holding them together is the whole point of the difficulty ladder above: a WAF being present is not the same as the target being hard, because most WAFs ship switched on by default with a CDN and were never tuned by anyone. The practical reading is that the barrier count tells you almost nothing on its own, and the only honest way to place a target is still to probe the endpoint you actually need. State of Web Access 2026, full report
.scrape/site/) with append-only authorship, timestamps, source, and reason on every observation. Three months later when the price field goes null, you read backwards through the trail to see which selector was used, which samples supported it, when it last passed, what changed on the site. The orchestrator is deliberately weak: it senses state, routes work, protects approval gates, and asks for human input on the schema/material-tradeoff decisions only, not every CSS path. Evidence-weighted consensus beats popularity-weighted ("another agent agreed" is the weakest form of review). Full Zyte writeup by Neha Setia NagpaliamNotaRobot.js, abuse-component.js, aps.js — it is PerimeterX rebranded and served from their own domain. Trust the code, not the label. (3) First-party cloaking: Home Depot serves PX-shaped scripts under random first-party filenames. You cannot identify defenders by checking hostnames anymore, you have to watch how the script behaves at runtime. (4) Lazy-loaded defenses: Ticketmaster ships a reCAPTCHA site key in the homepage JSON but the SDK only loads on login or checkout. Probing only the homepage misses everything. Multi-hop traversal (homepage → login → cart) is the minimum recon bar now. (5) The fingerprint dictates the budget: PerimeterX + behavioral biometrics + reCAPTCHA Enterprise on one page tells you exactly what tier of browser, what kind of proxy, and how slow your automation has to be. Recon is upstream of every other decision. The takeaway: stop asking "which vendor does this site use" and start asking "which stack does this site compose, and where on each layer do I look for cracks."navigator.webdriver = false when an AI agent drives Playwright. Google patched out CDP detection in V8. Neither change was announced. The signals every anti-bot tool relied on to flag automation just became officially unreliable, because AI agents browsing on behalf of real users broke the human-vs-automation binary.navigator.webdriver = true. That was the easiest detection signal in the industry, and most public anti-bot stacks were built around it (along with CDP-detection tricks for Chrome's DevTools Protocol). Two undocumented changes in 2026 have just made those signals soft: (1) Microsoft Edge returns navigator.webdriver = false when Playwright is driven by an AI agent on behalf of a user; (2) Google patched out the most common CDP-detection technique in V8. No release notes, no announcements. The reason is rational from the browser vendors' side: agentic browsing is now a legitimate use case (Anthropic Computer Use, OpenAI Operator, Browser Use, etc.) and the old binary doesn't apply. The implication for the guide and for production scrapers: any detection or bypass strategy that pivots on these flags needs to assume they are no longer reliable as either signal or counter-signal. Detection has to move up the stack, to behavioural ML, intent patterns, and network-identity layers (TLS JA4, IP reputation, WebRTC, DNS coherence), all of which are far harder to remove from the inside. DataDome's threat-research team published a longer breakdown if you want the technical specifics.navigator.storage and timing the flush. Incognito routes storage to RAM, normal mode hits disk. RAM is faster. That is the entire vulnerability.navigator.storage and measures the flush time. Under 0.1ms indicates RAM (incognito), above indicates disk (normal mode). No permission prompts, no API quirks the user can disable, runs in standard JavaScript on any page. Known caveats: RAM disks (used by some privacy-conscious users) trigger false positives unless the threshold is tuned (~0.01ms separates RAM-disk from incognito on test hardware); slow HDDs do not produce false positives because the technique detects suspiciously fast writes, not slow ones. For anti-bot: detecting incognito is a useful behavioural signal (legitimate buyers rarely shop in incognito; scrapers and abuse traffic over-index on it). For scraping: if your stealth stack uses incognito or per-session ephemeral storage to keep contexts clean, you are leaking a signal that is now trivially detectable. The fix is the same as for the broader timing-attack class: use persistent profile directories that hit real disk, accept the cookie/storage management overhead.playwright.launch() spinning up a local browser per script, run a persistent Playwright server on a dedicated box exposing a WebSocket endpoint, and have every scraping script connect("ws://host:3000") as a client. The page can't tell the difference, the API is identical. Five hard-won lessons from the build: (1) Binary choice matters as much as library choice. JS overrides of navigator.webdriver are themselves detectable (wrong property descriptor, wrong prototype chain); source-patched binaries like CloakBrowser remove the signal instead of masking it. (2) Headed via Xvfb beats headless, the virtual framebuffer means nothing looks headless because it isn't. (3) The two-slot trap: Playwright keeps TWO Chromium directories, a full build and a stripped chrome-headless-shell. Replace only the full slot with your patched binary and Playwright silently launches the untouched headless shell instead, you get 403s and the wrong version string with nothing in the logs explaining why. You must replace both slots and rename the headless one to chrome-headless-shell. (4) supervisord inside Docker manages the multi-process reality (Xvfb priority 10, Playwright server priority 20 with startsecs delay). (5) Concurrency = contexts, not instances. One browser, a pool of isolated contexts (separate cookies/storage), workers pull from an async queue, a 403 requeues with backoff and the worker grabs the next job. Proxy creds go per-context so a bad IP just retries with a fresh one. 16 concurrent contexts ran fine on a Ryzen 4650G mini-desktop. Full writeup · github.com/jhnwr/browser-service · YouTube.so library (common for the sensitive bits), Ghidra decompiles the C/C++ to understand the algorithm. (3) Frida hooks those functions at runtime via injected JavaScript, so you can log the inputs/outputs, bypass certificate pinning, or call the signing function directly to mint valid headers, no need to fully reverse the algorithm if you can just invoke it. (4) Run all of this on a rooted Android emulator from Android Studio for a controlled, disposable lab. Pair with HTTPToolkit or mitmproxy to capture the now-decrypted traffic and recover the API contract. MobileHackingLab offers a free Android Frida course with a certificate and CTF-style challenges, the fastest way in if the toolchain feels intimidating. The payoff: once you can mint the app's signed headers, you call its clean JSON API directly and skip every browser-layer anti-bot entirely. MobileHackingLab free Frida coursehyphens: auto CSS and measure rendered output.hyphens: auto is set and text overflows a container, the browser inserts soft hyphens at language-specific break points (so "hyphenation" becomes "hy-phen-ation"). The dictionary that drives this is OS-level on Android and macOS, but Chromium on Windows and Linux must bundle it at build time. Most people forking Chromium don't know this — the build artifact is large and the feature is invisible until you specifically test it. Joe (joe12387) demonstrated this is a detection vector: anti-bot scripts can render a known word in a known-width container with hyphens: auto, screenshot via Canvas, and compare the hyphenation positions against expected values for the claimed OS. A custom Chromium fork that fails to hyphenate at all (or hyphenates wrong) reveals itself instantly. Mitigation: ensure your build includes the hyphenation dictionary for the languages you claim to support, or run real Chromium binaries (not forks) under XVFB instead. Live PoC · github.com/joe12387_abck, datadome, cf_clearance, reese84), identify sensor.js challenge endpoints, figure out which requests trigger re-validation. For a moderately complex target like nike.com, this takes hours per session. With Burp's MCP server pointed at Claude Code, you capture the same session and prompt: "trace the _abck cookie lifecycle from home page through add-to-cart, identify all sensor payload endpoints, and explain the validation flow." Claude reads Burp's full history directly and produces the analysis in seconds. The pattern scales: build a reusable burp-antibot-recon Skill once, replay it across Akamai/DataDome/Cloudflare targets. If you work in this space and haven't wired it up, this is the unlock. github.com/PortSwigger/mcp-serverrequests or curl_cffi, the challenge is unsolvable without JS execution. The bypass is mundane: any headless browser (Playwright, Camoufox, Patchright) with JS enabled will solve it automatically. Persist the auth cookie (techaro.lol-anubis-auth) and reuse it across requests. The political angle: Anubis exists because AI scrapers (OpenAI, Anthropic, Common Crawl, ByteDance) were DDoSing small FOSS projects by ignoring robots.txt. It's a community response, not a commercial product. github.com/TecharoHQ/anubisrequests library sends a different cipher suite order than Chrome. httpx is different again. Even with a clean residential IP, if your cipher ordering does not match Chrome's, you are identified before the server processes a single header. Fix: use curl_cffi with impersonate="chrome124"it emits Chrome's exact TLS ClientHello. Also watch HTTP/2 SETTINGS frames, they contain window sizes and header table parameters that vary per client.geoip=True and it automatically aligns all five vectors. Do not simply disable WebRTC, it removes a feature that 99% of real users have, which itself becomes a bot signal.camoufox or rayobrowse to generate sessions, then curl_cffi with the extracted cookies for bulk collection. Rotate sessions every 30-50 requests.blocked_domains list to block tracking/CDN requests in headless mode, automatic proxy-aware retry on network errors, Response.follow() for easy link chaining. Install: pip install scrapling --upgrade.QuickProxy(countries=["FR","DE"]) API filters by exit country. The built-in cache means it does not hit proxy list APIs on every request. Usage: from swiftshadow import QuickProxy; proxy = QuickProxy(); session.proxies = {"http": str(proxy), "https": str(proxy)}. Important: free proxies have high failure rates and low anonymity, do not use for Akamai, DataDome, or PerimeterX targets. Best for scraping open/unprotected sites at scale without cost.pip install cocoindexconfigure sources (files, URLs, S3), define your chunker and embedding model, run cocoindex.build()done in under 10 minutes.What to watch next: the new QUERY method (RFC 10008, June 2026). One reason so many JSON APIs are POST endpoints is that the read carries a filter object too large for a URL, so developers drop it in a POST body. GraphQL does this for every query. The cost is that a POST is opaque to caches and intermediaries: it cannot be cached or safely retried, and nothing in the method tells you it was only a read. HTTP now has a verb for exactly this, QUERY, which carries a body like POST but is declared safe and idempotent like GET, with a matching Accept-Query response header advertising which query formats a resource speaks. It is a Proposed Standard, not yet widespread (framework support is landing, e.g. an open Spring PR), but two things matter for a scraper. First, as targets adopt it, a QUERY endpoint is an even cleaner signal than a POST that you have found a read-only data API. Second, because QUERY responses are explicitly cacheable, a polite scraper that respects cache headers can cut fetches the way it never could against POST. Watch the Allow header for QUERY alongside GET and HEAD.
When the internal API answers in an obfuscated shape: ProtoJSON. Finding the endpoint is sometimes the easy half. Large sites (Google properties are the textbook case) do not return clean labelled JSON, they serialise Protocol Buffers as deeply nested, positional JSON arrays with no keys, often prefixed with an anti-hijacking junk string such as
)]}' that you must strip before parsing. The data is all there, but a field is addressed by its path through the array, not by a name, so a star rating might live at P[7][1][15] and an English translation at P[7][2][15][1][0]. Two things make this tractable. First, treat the leading junk prefix as a known quantity and slice it off before json.loads. Second, map the indices once and encode them as named constants, and this is a task an LLM is genuinely good at: hand it a sample response alongside the rendered page and ask it to align visible values to array paths, then freeze the mapping into a parser. The indices are effectively a private schema, so they can shift without notice, which means the index map belongs behind the same field-coverage monitoring and self-healing trigger as any brittle selector. It is the backend-API strategy carried to its conclusion: you traded HTML parsing for array-path parsing, which is faster and more stable, but it is still a contract the site can change.curl_cffi for TLS, full Chrome headers via httpx or curl_cffi, random.uniform(1.8, 4.3) delays, requests.Session() for cookie accumulation, residential/mobile proxies for IP. Check your current fingerprint at tls.browserleaks.com/json.Detection risk by stack (lowest is best):
| Stack | Detection Risk | Limitation |
|---|---|---|
requests / httpx |
Very High | No browser rendering |
Scrapy |
Very High | No behavioural realism |
| Headless Browser | High | Headless traces (WebGL=null, missing extensions) |
| Stealth Headless | Medium | Partial spoofing, JS patches detectable |
| XVFB + Headed Browser | Lowest | Higher data consumption |
✓ XVFB virtual display (real X11 server, not headless flag)
✓ Fully headed Chrome (no
--headless anywhere)
✓ Nodriver for CDP without webdriver artefacts (or Camoufox for Firefox)
✓ Authentic TLS / HTTP-2 behaviour (the browser handles this for free)
✓ Humanised interactions (Bezier-curve mouse, variable scroll timing)
✓ Residential proxies, sticky session for trust accumulation
✓ Fingerprint coherence (UA + WebRTC + DNS + timezone all match exit IP)
Why XVFB beats
--headless even with stealth patches: headless Chrome reports HeadlessChrome in the user agent (fixable), missing extensions (probe-able), and zero GPU context (the real killer). With XVFB you get a real display, Chrome runs in headed mode, extensions load normally, and the GPU stack is whatever your server provides. JS patches still leave Function.prototype.toString() traces; XVFB does not.
The serverless angle: the conventional wisdom is that serverless cannot run a real browser. The trick is provisioning an X11 socket inside the container (
Xvfb :99 &, DISPLAY=:99 chrome ...) so Chrome runs headed on a virtual display. Lambda has hit memory limits historically, but ECS Fargate, Cloud Run, and Modal handle this comfortably with ~1GB memory per browser instance. The result: serverless infrastructure behaving like real users, not automation.
What still beats XVFB: C++ patched browsers like Camoufox (canvas, WebGL, audio at the binary level) and CloakBrowser (real extension probe profiles) close the remaining 10%. But for the 80-90% of targets where XVFB + Nodriver gets you in, the cost difference is significant. Camoufox: 200MB+ per instance. XVFB headed Chrome: same memory but works on any Chromium binary.
Modern anti-bot systems are trained to detect machines pretending to be browsers. The path forward is not better lies, it is fewer lies.
performance.now(). Both run below the JS hooks Camoufox, CloakBrowser, and PatchRight patch.The enabling primitive arrived in 2024 from Manuel at brokenbrowser.com: a one-liner that gets you a real
SharedArrayBuffer on any page, no special headers, by calling new WebAssembly.Memory({shared:true}).buffer. Drive a MessageChannel ping-pong with Atomics.add() inside it and you have a counter ticking at 100,000 Hz, micro-timing precision around 6 microseconds. Chrome marked it Won't Fix.
What this defeats:
× Camoufox (Firefox C++ patches at the browser layer)
× CloakBrowser (49 Chromium binary patches)
× PatchRight, undetected-chromedriver, Nodriver, Pydoll
× Every JS prototype patch (Function.toString detection is irrelevant when nothing JS is touched)
What still works: real hardware diversity. The future of stealth scraping is real consumer machines on real ISP IPs, which is essentially what high-quality residential proxy networks like Massive, Bright Data, and Oxylabs already provide. As detection moves into the CPU layer, the value of actually being real compounds.
This is also why akamai-v3-sensor works on Akamai v3: it never executes the WASM at all because it never reaches sensor.js. By bypassing at the TLS layer, you skip every detection layer above it.
Sources: Anthony Manikhouth (DataDome) and Manuel (brokenbrowser.com).
Why this matters for scrapers: if you are rotating cookies between requests to look like a fresh visitor, but the target site is reading your localStorage entry from the previous session, your rotation does nothing. Anti-bot vendors like Forter and Riskified have shipped variants of this for years. Cloudflare's
cf_clearance cookie now has localStorage backup in some configurations.
Storage layers a real reset has to clear:
✓ Cookies (HTTP and JS)
✓ localStorage and sessionStorage
✓ IndexedDB (every database)
✓ Service Worker registrations and Cache API entries
✓ FileSystem API (legacy but still works)
✓ Web SQL (deprecated but persists on older Chromium)
✓ ETag / If-Modified-Since headers cached at HTTP layer
✓ HSTS pin database (yes, browsing data can be encoded in HSTS pins, this is real)
Practical implication for scrapers: when you rotate sessions, do not just clear cookies. Either spin up a fresh browser profile each session (Playwright
context.close() + new context, or a fresh Camoufox BrowserContext), or run in an entirely isolated container. Half-measures leak state.
For the curious: the original Evercookie by Samy Kamkar in 2010 used 13 storage mechanisms. Modern browsers have removed several (Flash LSO, Silverlight, Java applets), but added more (Service Workers, BroadcastChannel, OPFS). The trick is alive and well, just modernised.
What was actually happening: the scraping continued. The traffic that used to say
"I am GPTBot" was now saying "I am Chrome 124 on macOS." Same content destinations, same fetch patterns, different label.
User-Agent is a string the client chooses to send. Polite scrapers send a real one. The scrapers you are actually worried about — the ones running at commercial scale on behalf of paying customers — send whatever string gets through.
robots.txt works on:
✓ Academic crawlers (Googlebot, Bingbot, academic research bots)
✓ Large AI labs (OpenAI, Anthropic, Google) that have reputational incentives to comply
✓ Hobbyist scrapers who read the rules and care
robots.txt does not work on:
× Commercial data brokers sending Chrome User-Agents
× Competitive intelligence tools running at scale behind residential proxies
× AI startups that have not publicly announced themselves
× Anyone whose business depends on data you do not want them to have
The implication for anti-bot systems: blocking by User-Agent is the weakest possible signal. Cloudflare, Akamai, and DataDome do not read robots.txt. They score TLS fingerprints, canvas hashes, behavioural timing, and IP reputation because those signals are harder to fake. User-Agent string matching is not a detection layer. It is a flag for voluntary compliance.
For scrapers reading this: if a target blocks GPTBot in robots.txt but has no real anti-bot scoring, the robots.txt is the only gate. Respect it. If they have Akamai or Cloudflare deployed, the robots.txt is decorative. The actual gate is the JA4 hash, the canvas probe, the IP reputation check. That is where this guide comes in.
Via a publisher conversation, May 2026.
The 2026 framing: web scraping is distributed adversarial systems engineering.
What modern infrastructure has to operate against:
• Fingerprinting systems (TLS JA4, canvas, WebGL, WASM SIMD)
• Behavioural detection (mouse physics, scroll timing, inter-request jitter)
• Anti-bot orchestration (Akamai EdgeWorker, Cloudflare Worker, DataDome middleware)
• Cloudflare interstitials and Turnstile challenges
• Dynamic runtime rendering (SPAs, hydration, lazy loading, service workers)
• Session-aware defences (trust accumulation, per-session scoring, unclearable cookies)
This is what makes tools like Scrapling architecturally interesting beyond "another Python scraper." It combines stealth browser execution, TLS fingerprint impersonation, adaptive element tracking that survives DOM changes, session-aware orchestration, proxy rotation, and MCP-based AI extraction. Not a scraper. A runtime.
The systems properties that matter now:
• Runtime orchestration (not just retries, state-aware crawl management)
• Observability (what failed, at which layer, on which request)
• Adaptive recovery (selector drift, DOM changes, anti-bot updates)
• State persistence (session trust, cookie chains, cross-request identity)
• Stealth execution (not a flag, an architecture)
• Infrastructure resilience (circuit breakers, session warmup, fallback tiers)
Once AI agents start interacting with the web autonomously at scale, reliability becomes a systems problem first and a parsing problem second. The pipeline that Firecrawl, Crawl4AI, Stagehand, and Scrapling are converging on is not "scraper plus LLM." It is a resilient extraction runtime with LLM as one processing layer among many.
Framing via D4Vinci (Scrapling author), May 2026.
What they found:
12.5% of analysed sites deployed fingerprinting-related scripts consistent with harvesting. A subset replicated vendor-specific telemetry from PerimeterX, Incapsula, Akamai, Adyen, and hCaptcha, not to defend themselves, but to collect and replay the same signals against those vendors.
The mechanics:
Services like
impersonate[.]pro openly advertise "comprehensive TLS, HTTP/2, HTTP/3, and JavaScript fingerprint collection." In Discord and Telegram communities, bot developers discuss embedding custom JavaScript on real websites specifically to harvest fingerprints from genuine visitors. The goal: build inventories of authentic device profiles that can be injected into automated sessions.
The PerfectCanvas mechanism from Bablosoft is the clearest example. Their documentation describes exactly the pattern:
• Render canvas on a real remote machine with a real GPU
• Send the canvas output to the automation server
• Inject it into the headless browser's response to the canvas probe
This is the harvesting-and-replay model made explicit. Instead of spoofing canvas values (detectable via inconsistency), you replay values from a real Mac. The fingerprint is genuine. It just came from a different device.
Genesis Marketplace established the precedent: ~323,000 compromised browser environments for sale, each bundled with a real device fingerprint and a custom Chromium extension that injected the victim's browser profile into attacker sessions. F5 Labs and Europol both documented this. Castle's report shows the same approach is now commercialised at scale for bot traffic, not just account takeover.
What this means for scrapers:
The arms race has a new dimension. Anti-bots are scoring fingerprints. Bot services are buying real fingerprints to replay. Defenders are now building for replay conditions, not just spoofing conditions. This is why:
• Canvas/WebGL probes are increasingly paired with behavioural and timing signals (harder to replay than static values)
• WASM SIMD CPU probes (above) are valuable precisely because they are harder to harvest and replay than JS-layer fingerprints
• Anti-bots are introducing controlled variability in their own client-side scripts so that even valid payloads can't be reverse-engineered and replayed reliably
The implication for this guide: when a stealth browser passes the canvas probe, it may not be because it spoofed the hash well. It may be because it replayed a real hash that was never flagged. The distinction matters because vendors will move toward replay-resistant probes, making the harvest-and-replay model progressively harder. WASM SIMD (which requires real hardware timing) is an early example of a replay-resistant signal.
Source: Fingerprint Harvesting in the Bot Ecosystem, Castle Research, Antoine Vastel, April 2026.
How a regenerated crawler earns promotion:
• It must agree with the LLM oracle on every item, not on average. A high mean is not good enough; one disagreement fails the batch.
• It must return the exact same record count as the oracle pass.
• It must hold across at least three independent samples, so a lucky single page cannot promote a broken crawler.
Tiered model escalation keeps the cost sane. A cheap model runs the regeneration by default; the loop only escalates to a stronger model when nothing clears the gauntlet. Whatever gets promoted is still free deterministic code, so the one-time model cost amortises across every later run.
This matters because "self-healing scraper" demos are easy and trustworthy self-healing is hard. Any model can produce a plausible rewrite. The engineering is in the verification that decides whether the rewrite is correct before it touches production data, which is the same adversarial-verification idea this guide's AI Workflow is built around. It pairs naturally with the agentic reverse-engineering shift in the next card: the model drives the toolchain, but a gauntlet, not the model's confidence, decides what is true.
The proof that separates it from a demo: the author ran the whole loop live against a real public site over real HTTP, not a saved fixture. The cheap model could not clear the gauntlet, so the loop auto-escalated to a stronger model, promoted a crawler, and that promoted crawler then extracted a fresh page (title, price, stock, URL, cover image) for zero cost with zero further model calls. That is the claim that matters: not "an LLM fixed my scraper" but "the system decided, on its own, that the fix was trustworthy enough to run unattended, and it was right." The honest weak spot is in the open too: holding a greedy oracle to exact agreement across a twenty-item listing is hard, and that is where it currently strains.
Pattern and live proof of concept shared publicly by the author of Crawloop (Apache-2.0 alpha POC, github.com/Jimmynycu/Crawloop), June 2026. Honest status from the README: a working POC without the managed proxies, scale, or dashboards the funded self-healing tools (Kadoa, ScrapeGraphAI, and others) ship.
.so in Ghidra, trace the call chain, confirm with Frida, rebuild in Python) is exactly the kind of multi-tool loop an agent can now coordinate. You point a coding agent at a target, it spins up specialist sub-agents (engines, impersonation, detection, architecture, fingerprints), runs them in parallel, then synthesises and stress-tests the result.
The honest caveat is the same one the reverse-engineering community draws: agentic workflows compress the process, they do not dissolve every defence. Some obfuscation classes (heavy virtualisation, bytecode VMs) stay resilient, which is precisely when you fall back to the oracle approach from the mobile section rather than a clean offline rebuild. Treat the agent as a force multiplier on a method you already understand, not a replacement for understanding it.
And the obfuscation side is adapting to the agents specifically. The same practitioners who teach automated deobfuscation (SMT solving, symbolic execution, MBA simplification, program synthesis to recover VM handlers and bytecode) now report protections deliberately engineered to break those pipelines: anti-agentic patterns. Analysis traps that detect a symbolic-execution or instrumentation harness and change behaviour under it, and runtime-bound semantics where a function's meaning depends on live state an offline solver cannot supply, are built to make an automated loop stall or draw a confidently wrong conclusion. The arms race did not end when agents could drive the disassembler; it moved up a level, and the counter to a trap you cannot automate around is still a human who understands what the loop was trying to do.
The defensive counterpart: adversarial, evolutionary obfuscation. The same shift explains why classic JS obfuscation is effectively dead against a capable agent. Packing a script, virtualising it, or flattening its control flow only bloats the code; a frontier model with a sandbox takes those apart in minutes, because the transformation is mechanical and therefore reversible by a mechanical process. The technique defenders are moving to instead is generative and uses the attacker's own tool as the fitness function. Rather than hiding a detection probe by wrapping it, you take a method that is API-adjacent and plausible (a dead-end bot inquiry that looks like ordinary telemetry) and have a model morph it together with the real probe you want to conceal, then point a council of analysis agents at the result and ask them to state its purpose. Whatever they correctly identify, you feed back as the next generation's target to disguise, and you repeat. After only a few rounds the reported case is that the agent council could no longer determine the exact mechanism by which detection was happening, not because the code was denser but because its observable behaviour had been evolved to read as something benign. The lesson cuts both ways for a scraper: the obfuscation you meet on a serious target is no longer a puzzle with a fixed solution your agent will grind out, it is a moving artifact shaped specifically to survive being read by an agent like yours, and the tell you are looking for may have been deliberately grown to look like innocent instrumentation.
A concrete cold-start case. A researcher pointed an agent that pairs a model with a sandboxed VM (Manus AI) at a live Akamai deployment on a real luxury store, with no prior notes and no internal wiki, and asked only that it study how the protection works, deobfuscate the client sensor, and enumerate which parameters feed the score. The agent fetched and instrumented the actual live script in its VM, then returned a structured map of the anti-hook layer, the challenge flow, and the TLS gate. The point is not a finished bypass (none was shipped) but that the expensive, human-gated part, reading minified obfuscated telemetry code and rebuilding its logic, was done by the agent running and checking its own work rather than a person spending days renaming variables.
The guardrail gap is the real story. Ask a guarded chat assistant to deobfuscate a production anti-bot sensor and enumerate its scoring signals and you hit a refusal, because that is squarely inside cybersecurity guardrails. An agent product wired to a VM took the same task and ran it. The economic consequence is what matters for the arms race: the cost that kept most protections standing was the human reverse-engineering hours, and an agent that verifies its own deobfuscation moves that gate. Defenders should now assume sensor logic is cheaper to map than it used to be, and lean harder on the layers that do not live in the client script.
Framing from "Deobfuscation in the Age of Agentic Reverse Engineering" (REcon 2026), practitioner demos of multi-agent RE pipelines, and a documented cold-start agentic mapping of a live Akamai sensor (The Web Scraping Club, Lab #108, June 2026). The agent's specific findings are reportage, not independently re-verified, and operational specifics were redacted at the source.
• Obscura is a headless browser written in Rust that ships its own V8, speaks the Chrome DevTools Protocol, and runs as a single binary with no Chrome and no Node.js. Roughly 30MB resident against 200MB-plus for headless Chrome, with per-session fingerprint randomisation and a DOM-to-Markdown mode for feeding pages straight to a model. Be honest about maturity: it is an early v0.1.0 with self-reported numbers, so star it and benchmark it yourself rather than betting production on it today.
• Crawl4AI turns selected pages into clean Markdown or structured data built for agents, RAG, and pipelines. It is the extract-and-shape half of the stack.
• SearXNG is a self-hosted search layer that finds the candidate URLs in the first place. It is the discover half.
Together they describe a small, controllable agent web-context loop: discover, fetch, extract, cache, cite, with each stage owned by a light tool you can host yourself rather than a heavy browser doing all four jobs badly. The takeaway is not "switch to these tools." It is that when an agent is the consumer, the cost of rendering pixels nobody looks at is pure waste, and the tooling is starting to reflect that.
Tools surfaced publicly by practitioners in 2026: Obscura (Rust headless, Apache-2.0/MIT, early), Crawl4AI (Apache-2.0, 68k+ stars), SearXNG (self-hosted metasearch).
Why the idea is clever. A traditional CAPTCHA asks for an answer a script can compute or outsource to a solving farm. A behavioural CAPTCHA scores how the answer was produced: typing cadence, edit and pause patterns, the order in which UI elements were touched, time spent reading versus writing. Those are expensive to fake convincingly because they are emergent properties of a real person working through a task, not a field you can fill in. The out-of-order tell (rating before submitting) is a neat tripwire: it catches automation that fills every field it sees without modelling the human workflow the form implies.
Why it still falls. Within a day of the public demo, another engineer bypassed it consistently with an LLM plus Playwright. The sequence is familiar from the rest of this guide: first attempt scored too low and was rejected, the approach was tuned, the second and third attempts passed, including a live run. Behavioural scoring raises the cost of automation, it does not create a wall, because a scripted agent can be taught to produce human-shaped timings and to touch the form in the order a person would. The lesson cuts both ways: if you defend, behavioural signals are a strong layer but not a final one, and you must assume they will be modelled; if you scrape, the modern bar is not "submit the right value" but "reproduce the human process that produced it," which is exactly the territory automation-protocol and interaction-timing detection already live in.
The healthy norm on display. Both the defence and the bypass were published openly, demo and method in the open, framed as understanding security rather than breaking it. That is the same posture this guide takes: the techniques are dual-use, and studying them in public is how both sides get sharper.
StoryCaptcha by Tyler Richards (stackedqueries), a stated proof-of-concept and not production-ready by the author's own note; public AI-plus-Playwright bypass write-up, 2026.
curl command and a tool parses it, replays the same method, headers, body, cookies, and auth inside an actual Chromium driven over the automation protocol, clears the browser-side friction (Cloudflare's managed challenge, a Turnstile widget), and hands back the final response. The point is not a new bypass, it is that the browser-grade request is expressed as the one-liner you would have written anyway.github.com/seifreed/Curlwright): it parses a curl invocation, supports the common flags (-X, -H, -d/--data-*, -b cookies, -u auth, -x proxy, -L, -k, --max-time), and runs it through a genuine Chrome rather than a spoofed fingerprint. Two design choices generalise beyond it.
Stealth below the automation layer. Rather than faking a browser with injected JavaScript, the current generation of these wrappers drive the real Chrome binary through a patched automation stack (Patchright-style) so the automation tells (the
Runtime.enable CDP leak, navigator.webdriver, headless markers) are neutralised at the protocol level, and falls back to a CDP-free driver (nodriver-style) for the hardened "Just a moment" managed challenge. This is the same lesson the detection and library sections of this guide keep arriving at: on protocol-fingerprinting targets, driving a real browser without the standard automation surface beats patching a headless one. Persisting and re-importing the solved-cookie session (so a warmed-up cf_clearance can be reused across calls) is what makes the curl ergonomic actually practical at more than one request.
Machine-readable output for pipelines. The other generalisable idea is emitting structured JSON and even SARIF alongside the response, so a browser-grade fetch can drop straight into CI and security tooling that already understand those formats. It is a small thing that quietly moves browser-based fetching from an interactive task to a scriptable pipeline step. The honest constraint of the whole pattern is the one its own authors flag: because it drives a real Chrome, it needs that browser present on the host and carries the cost and weight of a full browser per protected request, so it earns its place on the hard targets, not as a default replacement for a plain HTTP client on everything.
Pattern reference: CurlWright (seifreed, open source), 2026, plus the Patchright and nodriver engines it builds on. Described here as a tooling-ergonomics pattern, not an endorsement of any one tool.
Check your own
fingerprint first
Before you bypass anything, you need to know what your setup is leaking. These tools show exactly what anti-bots see when your scraper connects. Run your scraper through them, not just your browser.
One habit worth building: re-run these checks on a schedule, not once. Detection scripts are not static. A vendor that probed one set of surfaces last week probes a different set this week, so a fingerprint audit you did a month ago is already stale. Treat your own fingerprint as something to monitor continuously, the same way the other side treats yours.
The wire-level capture loop: how you actually close the gap on a blocked request
accept-encoding: gzip, deflate, br, zstd since Chrome 123, so a request still sending the three-value version no longer looks like current Chrome). You cannot fix what you cannot see, so you need a proxy that records the real wire.reese84 cookie echoed as an x-d-token header will turn up in three places: the response that minted it, the cookie that carried it, and the header that spends it. That chain, not the endpoint, is the real output of reading a session, because it tells you which requests your script has to make first. (3) Write a first pass with a browser-grade TLS client, routed through the same capture proxy. (4) Diff your request against the browser\'s in wire order and fix every difference until the diff is empty. The differences that block you are usually one token wide, a missing zstd, a stray Host header on an HTTP/2 request, priority not sent last, or headers in alphabetical order which is itself a dead giveaway that they came out of a dictionary rather than a browser. (5) Replay one captured request over and over and watch for the flip from 200 to 403. That number is how many calls a single warm session survives, and you set your rotation threshold just under it, which is how the session-stickiness advice in the proxy section turns into an actual figure instead of a guess.Content-Length to the bottom of the header block when chaining to an upstream proxy, corrupting the exact ordering you opened the tool to inspect. Verify your tool preserves wire order before you trust its output. Turn off the DevTools "disable cache" option while recording, because it injects Cache-Control and Pragma headers a real navigation never sends and you will copy them into your script. Record from a clean, ordinary profile, not incognito and not one that merely had its cookies cleared, since both produce artefacts like extra Client Hints on the first request. And route only the client you are debugging, not the whole system, or the session drowns in traffic from every app on the machine. One more detail people miss: header order is per request type, so a navigation, an XHR, a subresource and the anti-bot\'s own endpoint each have their own order. Capture the specific request you intend to replay.Two capture mistakes that waste a whole afternoon
powhttp, which is built for exactly this comparison and has almost no documentation, so budget an hour to learn it. Whatever you choose, verify it preserves header order before you trust a single diff it shows you, because an interception proxy that quietly reorders headers turns the tool you opened to inspect ordering into the thing corrupting it.Record from an ordinary profile, and understand what incognito actually leaks into your capture. The advice to avoid private windows when recording is usually given without a reason, so it gets ignored. The reason is specific: a session recorded in Incognito or Guest mode carries artefacts a normal navigation never sends. You may see
Sec-Fetch-Storage-Access: none, and the payloads that anti-bot scripts assemble can contain stricter third-party-cookie and storage-partitioning signals that reflect the private context rather than the browsing you are trying to imitate. Replay that capture and you are faithfully reproducing a browser state no ordinary visitor is in, which is a coherent-looking request that is coherent about the wrong thing. Practitioners working request-based DataDome flows report this as one of the most common causes of a session that was recorded wrong from the first byte, where the TLS is right, the header order is right, and the replay still fails.How production scrapers
are actually built
From a single Scrapyd daemon to multi-region ECS clusters. Twelve real pipeline architectures, from simple to enterprise-scale, with every component and data flow mapped out.
The simplest production setup. One server, Scrapyd managing spiders via JSON API, ScrapydWeb as UI. Good for <50 spiders and teams without Kubernetes. Deploy with scrapyd-deployschedule via /schedule.jsonmonitor at port 6800.
The AI Workflow
graph + adversarial + five algorithms
A genuine AI-agent scraping system, designed from scratch. Two agents working through a queryable graph of vendor and selector history, with six mathematical concepts each replacing a heuristic that would otherwise rule the system. Below the conceptual diagram, you will find what this looks like when you actually run it in production: real services, real protocols, real bottlenecks, and the cost numbers it should add up to.
Beta(95, 5) means 95 successes in 100 tries, very confident. Beta(3, 0) is 3 from 3, but the credible interval is wide. Routing weighs evidence, not just point estimates.Concepts deliberately rejected as theatre for this scale: GNN training (graph too small), max-flow/min-cut (no mapping), causal do-calculus (impractical in production), game-theoretic minimax (no concrete operation), Markov chains for traversal (overkill), learned embeddings (cost > value).
The architecture · conceptual view
A genuine AI-agent scraping architecture, built around three ideas you won't find in published frameworks. (1) Graph memory. URLs, selectors, vendors and outcomes are nodes, connected by edges. When one vendor changes its sensor, every URL on that vendor inherits the learning automatically. (2) Adversarial verification instead of self-healing. The scraper agent ships an answer + a confidence score, the verifier agent's job is to disprove it. Only outputs that survive the disproof attempt reach gold storage. The detail that makes this work is keeping the verifier context-blind: run it in a fresh session with no memory of how the answer was produced, so it judges the output on its merits instead of inheriting the generator's rationalisations. A reviewer that sat through the generation will quietly accept the same shortcut that produced the bug; a reviewer handed only the result, the schema, and the live page has to re-derive correctness from scratch, which is exactly the property you want. The same applies when an agent rewrites a broken scraper: benchmark and review the regenerated code from a clean context, not inside the conversation that wrote it. (3) Graph-theoretic intelligence on top of the memory. PageRank ranks vendors by operational criticality, community detection auto-distinguishes structural failures from selector drift, shortest-path traversal bootstraps strategies for new URLs from their nearest known-working neighbours. Most of the system runs cheap, verification runs only on the hard cases.
What this looks like in production
Real services, real protocols, real bottlenecks. Drawn for 10M-50M URLs/day. Each rung in the Scraper agent is annotated with the actual scraping technique that does the work, not just the box that runs it. Color-coded dots mark each flow direction so you can trace any path through the system.
pydantic for structured output, networkx for the three graph algorithms, scipy.stats.beta for Bayesian state, small custom services for Thompson sampling, KL drift, and the PID loop, boto3 for SQS/Kinesis/S3. Frameworks shine when you have 30 chains and 10 tool integrations to coordinate. We have 2 agents and a queue.
Self-Healing Scraper
powered by Claude
Scrapy spiders break when sites change their HTML. Instead of manually fixing selectors, this architecture uses Claude to detect failures, analyse the new page structure, and write corrected selectors automatically, without human intervention.
You are a web scraping expert. A Scrapy spider broke because the site changed its HTML.
Old selectors (no longer working):
title: h1.product-title::text
price: span.price-now::text
image: img.main-image::attr(src)
New page HTML (truncated):
{{ page_html[:8000] }}
Return ONLY valid JSON with corrected selectors:
{"title": "...", "price": "...", "image": "..."}
The lab-to-production gap: running thousands of pages unattended for days
Handle failures at the right layer. A
try/except around your parse code never sees a network-level failure, because the request fails before your callback runs. You need an errback to catch DNS errors, timeouts, and connection resets, and a helper like get_retry_request to requeue with proper accounting instead of silently dropping the URL.Then monitor outcomes, not just uptime. A tool like Spidermon validates each run against expectations (item counts, field coverage, error rates) and alerts when a run drifts, so a half-broken crawl is caught in minutes instead of discovered the next day. For deployment, the ladder runs from Docker to Scrapyd to a managed cloud, and scrapy-redis earns its place only once you genuinely need a distributed, shared request queue across workers, not before.
HTTP 403 on every retry, every proxy, every subreddit. The retry logic was fine, proxy rotation was fine, the user-agent set was varied. The cause was upstream of all of it: the shared residential proxy pool had been fingerprinted and burned against www.reddit.com, and you cannot out-rotate a poisoned pool. The fix was a single hostname change to old.reddit.com, the same JSON API with the same response shape, but served by a subdomain whose bot-detection thresholds are far looser because most human traffic left it years ago. Success snapped back to 92% with zero retries.
old. or m. subdomain whose WAF rules differ from the main host. Third, and most important for unattended systems, the platform never sends you a deprecation notice: your failure-rate chart is the notice. The same scraper later survived Reddit removing its public JSON endpoints entirely by falling back to RSS feeds behind a circuit breaker, with the degraded fields tagged honestly in the output rather than silently passed off as complete. Monitoring is what turns a silent collapse into a five-minute alert and a planned fallback.
download_delay and any robots.txt Crawl-delay kept as a hard floor so politeness is never undercut. The subtle part is the failure response: when a site starts blocking or rate-limiting, the crawler should back off (double the delay, or wait exactly what a Retry-After header asks) until that clears, then recover. Here is the trap that catches naive latency-based throttling: blocked and challenge pages usually come back faster than real ones, because a captcha wall or a 429 stub is cheaper to serve than a rendered page. A throttle that only watches response speed therefore reads a wall of blocks as the server having spare capacity and speeds up, which is precisely the wrong move and turns a soft rate-limit into a hard ban. The fix is to make the throttle react to block signals (status codes, challenge markers, Retry-After) and not to latency alone, so that getting slower under pressure, not faster, is the built-in reflex.
Structure the scraper so an agent can fix one selector, not rewrite the project
price field. An agent (or a person) can be handed just that class, its test, and the new HTML, and asked to repair that one extractor, with no risk of collaterally rewriting pagination or the request logic that still works. The Scrapy ecosystem packages this as web-poet and scrapy-poet: Page Objects plus dependency injection, so each page type is an isolated, swappable unit. The smaller the unit a fix touches, the easier it is to trust the fix, which is the same principle the promotion-gauntlet card argues from the verification side.From prompting to loops: the self-correcting maintenance cycle
Hardening the loop: what a production self-healer needs that a demo does not
degraded in the graph, and escalates to the human gate with the diff it could not resolve. A self-healer that cannot give up is not autonomous, it is just expensive.Build vs buy:
the number that decides
The most common mistake is treating "can I bypass it" as the only question. The real production question is "what does each successful record cost, and is rolling my own cheaper than paying someone else." Here is the honest math, with the caveat that exact prices move constantly, so treat these as orders of magnitude, not quotes.
Self-hosted stealth stack vs managed API
| Approach | Monthly cost driver | Rough cost | Best when |
|---|---|---|---|
| Self-hosted: HTTP + curl_cffi + residential proxies | Proxy bandwidth (the dominant cost), small server | Proxy GB at roughly 3 to 8 USD/GB, plus ~50 to 200/mo compute | High volume on targets that yield to TLS impersonation, where you control bandwidth use |
| Self-hosted: Camoufox/CloakBrowser cluster + residential proxies | Server CPU/RAM for browsers, plus proxy bandwidth (browsers burn far more GB) | ~200 to 1,200/mo compute, plus heavy proxy GB; engineering time to maintain | Hardened targets that need a real browser, at volumes where per-request API fees would exceed infra cost |
| Managed API (ScraperAPI, Zyte, Bright Data Web Unlocker, Scrapfly) | Per successful request, anti-bot handling included | Roughly 1 to 5 USD per 1,000 requests (more for JS-render / hard targets) | Low-to-medium volume, or hard targets where engineering time is worth more than the per-request fee |
The bill is mostly waste, not price
A perspective worth holding while you read the rest of this guide
A warning about the one clean number: cost per thousand requests
The other number to distrust: a vendor's success rate
Spend the model once: build-time AI, not runtime AI
fetch() the API directly instead of reloading), and the moment you have done that, the browser and any model driving it are dead weight you can drop. The one genuinely useful role for a model at build time here is reading the real wire traffic, the exact header order and the JA4/TLS fingerprint that a HAR export or DevTools cannot show, and reproducing it in request code, which is precisely the analysis you only need to do once.
The cost nobody budgets for: raw browser runtime speed
What happens to
50 million rows
Bypassing detection is the part everyone writes about. But getting the data is only the start. The questions that actually decide whether a scraping operation survives are about what you do next: where the data lands, how you avoid storing the same thing twice, and how you notice when a site quietly starts feeding you garbage.
Storage: stop dumping JSON into a folder
Deduplication: the same item will arrive many times
RFPDupeFilter backed by Redis) checks membership in constant memory with a tiny, tunable false-positive rate. For distributed crawls, a shared Redis set keeps every worker honest so two workers never fetch the same page.last_seen timestamp so you can tell a genuine update apart from a re-scrape, and upsert rather than blindly insert.Data poisoning: when the bypass succeeds but the data is fake
200 OK with a degraded body: an empty list, sanitised prices, a tarpit that drips bytes forever, or yesterday's snapshot frozen in place. If your scraper treats the status code as the success signal, you will log thousands of green requests and ship corrupt data. Validate the shape and the statistics, not the status. The question is never "did I get a 200", it is "did I get roughly N records like yesterday, with prices in the range I expect, and the fields I depend on populated". Wire that check into the pipeline so a silent drop to ten percent fill rate pages you instead of flowing downstream.One operational habit that pays for itself: keep a small Targets table as the single source of truth for every endpoint you depend on. Endpoint path, required headers, cookie shape, the schema version you last validated against, and the expected record count. When a site renames
/api/v2/ to /api/v3/ or reshapes a field, you change one row instead of hunting through scraper code. It costs half a day to write and saves you the morning you would otherwise spend discovering, after the fact, that a fortnight of data is empty.
The quiet failure mode: a 200 with nothing in it. Poisoning replaces your data with something wrong. There is a milder and far more common variant that replaces it with nothing at all, and it is harder to notice because every dashboard stays green. A verification interstitial or a soft rate limit answers with HTTP 200 and an empty result set rather than a challenge or an error, so the request succeeds, the parse succeeds, and the record count is zero. Search endpoints are where this shows up most, and practitioners tracking one major engine through mid-2026 reported exit addresses in some regions failing this way the overwhelming majority of the time while the success metric never moved.
The defence is the same instinct as the poisoning check, applied to volume rather than values. Alert on record count per request, not on status code. A run that returns 200s and zero rows is a failing run, and it should page you exactly as loudly as a run of 403s. The operational counter that works is unglamorous: take a fresh session on one sticky address, request a small amount, confirm real records come back, and only then increase the rate on that same session, holding the identity still rather than rotating into a new one. You are looking for the point at which results thin out, which arrives before the point at which requests are refused.
The status code is a two-way signal: soft 404s, crawl budget, and hallucinated URLs
The defence is the same instinct as the poisoning check, applied to volume rather than values. Alert on record count per request, not on status code. A run that returns 200s and zero rows is a failing run, and it should page you exactly as loudly as a run of 403s. The operational counter that works is unglamorous: take a fresh session on one sticky address, request a small amount, confirm real records come back, and only then increase the rate on that same session, holding the identity still rather than rotating into a new one. You are looking for the point at which results thin out, which arrives before the point at which requests are refused.
200 OK can hide failure on the way in, when a tarpit feeds you poisoned data. The same dishonesty runs the other way too, and it is worth understanding from both chairs because it shapes how crawlers (yours, Google's, and an LLM's) treat a URL.Many JavaScript frameworks, by default, answer a request for a URL that does not exist by rendering a "not found" view and still returning 200 OK in the header. The page says the content is gone; the server says everything is fine. Google calls that mismatch a soft 404, and it is not harmless. A real
404 or 410 tells a crawler to stop coming back. A soft 404 keeps the dead URL in the queue, because the server never admits the page is gone, so the crawler keeps re-fetching nothing.
When the data is not HTML: parsing documents
grand_total: 369963 from a blurry receipt is its own kind of soft 404, output that looks clean and is wrong. A schema-consistent JSON contract, a numeric confidence on every field, and an explicit human-in-the-loop lane for the low-confidence tail are what turn document parsing from a demo into something you can trust at volume. The same discipline applies whether the input is a scraped PDF, an emailed invoice, or a photographed receipt.Intercept mobile app traffic
before it hits any anti-bot
Mobile APIs serve the same data as the web, but with weaker protection. No Cloudflare, no JA4 fingerprinting. Intercept the traffic once, replicate the call forever.
git clone https://github.com/newbit1/rootAVD.git
cd rootAVD
# Verify AVD is accessible
adb shell
# List your AVDs
./rootAVD.sh ListAllAVDs
# Copy the first command from the output and run it
# e.g: ./rootAVD.sh system-images/android-30/google_apis_playstore/x86_64/ramdisk.imgadb not found? Add to ~/.zshrc: alias adb='/Users/$USER/Library/Android/sdk/platform-tools/adb'# macOS
brew install --cask http-toolkitimport curl_cffi.requests as requests
resp = requests.get(
"https://api.targetapp.com/v2/listings",
headers={
"Authorization": "Bearer <token_from_http_toolkit>",
"X-App-Version": "4.2.1",
"User-Agent": "TargetApp/4.2.1 (Android 11; SDK 30)",
"Accept": "application/json",
},
impersonate="chrome120"
)
data = resp.json()- Property portals, classifieds, marketplaces
- Apps where the web version is heavily protected
- Data only available in the mobile app
- Targets using simple Bearer token auth
- Any app that doesn't pin SSL certificates
- Apps with SSL pinning block interception
- Some apps crash on rooted devices
- ARM-only apps may not run on x86 emulators
- Tokens expire, need refresh logic in scraper
- App updates can silently change endpoints
If the app blocks interception it likely uses SSL pinning. Use Frida or objection to bypass it at runtime, or use Burp Suite with the Xposed + TrustMeAlready module for a more permanent bypass.
When the signature lives in native code
native, with its body on the far side in compiled ARM inside a .so library. This is where most people give up on the mobile route. It is also where the mobile route gets genuinely durable, because once you reproduce the signing you are no longer tied to a running app at all.
.so libraries an app ships and finds which classes declare native methods. Structure you can script, not readable source.JADX decompiles Dalvik back to Java. It is how you read the managed side and find the exact class and method that crosses into native code. It stops at the
native keyword, the handoff point.Ghidra is the NSA's open framework. It disassembles a
.so and decompiles it to pseudo C. It is the only tool here that reads native code, so the work centers on it. Run it headless so the workflow scripts cleanly and repeats exactly.Frida injects a JS engine into the running process so you can hook and call functions live, and confirm your static reading against what the app actually does.
.so libraries does the sensitive work. Interceptors attach the anti-fraud headers. Name the layers before you open anything, so you target the one library that matters instead of all of them.A small library is a good sign. A signing lib of a few hundred kilobytes has little room for a heavy obfuscator, and auto analysis that finishes fast with zero decompile failures tells you the binary is not packed or virtualised. C++ symbols that survived give you the function names for free.
Some values are not in the file. A fixed AES IV can live in
.bss, which is zero-filled on disk and only set at runtime. Hash, HMAC, and Base64 modes do not care because their output is fully determined by input and key. The AES modes do, so you read that one value from the live process once and bake it in.
Scraping jargon
in simple terms
Every term that makes scraping documentation confusing, explained with an analogy.
/robots.txt telling crawlers which paths to skip. Works on voluntary compliance only. Googlebot and GPTBot respect it. Commercial scrapers send a Chrome User-Agent and walk straight past it. Analogy: a "staff only" sign. Anyone who cares about signs obeys it. Anyone who does not care walks in anyway.The guide as a graph
because the links are the argument
Twenty sections and a hundred and forty field notes are a pile, not a model. What makes this subject learnable is that almost everything in it is an instance of about eighteen principles, and those principles attach to the layers a request is judged at. 130 nodes, 188 typed edges, and every section of the guide represented. The rings run outward from the spine: principles, then the layers a request is judged at, then methods, then tools and vendors, with the measured evidence and the case law on the rim. Click any node to see what it connects to, and why.
Text outline of the whole graph
The questions I
actually get asked
Twenty-two of them, answered in a paragraph each. Every answer points at a section above where the long version lives.
What is the best library for bypassing Cloudflare in 2026?
Camoufox is the strongest open-source option for bypassing Cloudflare in 2026, achieving a 100% pass rate in March 2026 benchmarks. It patches Firefox at the C++ level using Mozilla's Juggler protocol, making it undetectable via JavaScript inspection. For HTTP-only scraping, curl_cffi with impersonate='chrome131' handles most Cloudflare targets without a full browser.What is JA4+ TLS fingerprinting and how does it affect web scraping?
JA4+ is a TLS fingerprinting standard that identifies scrapers before any HTTP headers are exchanged. It hashes the TLS ClientHello fields (cipher suites, extensions, ALPN) in a sort-stable way that survives Chrome's extension order randomisation. Cloudflare deploys JA4 in a Rust crate at CDN edge, Akamai in an EdgeWorker. Python's requests library has a unique JA4 hash that gets blocked instantly. The fix is curl_cffi, which impersonates real Chrome TLS down to HTTP/2 SETTINGS frames.How do you bypass Akamai Bot Manager in 2026?
Camoufox (uses Juggler protocol, no CDP artifacts). Combine with residential or ISP proxies since Akamai flags datacenter ASNs instantly. Set geoip=True in Camoufox to align WebRTC, DNS, and timezone with your proxy exit country.What is the difference between residential and datacenter proxies for web scraping?
How do you scrape JavaScript-rendered websites with Python in 2026?
Camoufox (Python, patches Firefox at C++ level, bypasses Cloudflare), PatchRight (undetected Playwright drop-in, bypasses Kasada), or scrapy-stealth middleware (adds TLS fingerprinting and browser engine to Scrapy). For AI-powered extraction, Crawl4AI (60K stars) and Firecrawl (111K stars) convert pages to clean Markdown. Avoid plain Playwright without stealth patches — navigator.webdriver=true is trivially detected by all major anti-bots.What is curl_cffi and why is it better than requests for web scraping?
curl_cffi is a Python library that wraps libcurl with BoringSSL patches to produce exact Chrome and Firefox TLS fingerprints. Unlike Python requests (which has a unique JA4 hash that anti-bots recognise instantly), curl_cffi sends a ClientHello identical to a real browser including HTTP/2 SETTINGS frames. It is 10-50x faster than browser automation and works as a drop-in requests replacement: curl_cffi.requests.get(url, impersonate='chrome131').How do I intercept mobile app API traffic for scraping?
curl_cffi for production scraping. Mobile APIs serve the same data as the website but with far weaker anti-bot protection — no Cloudflare, no JA4 fingerprinting.What is the best Scrapy anti-bot middleware in 2026?
Why does nodriver beat patched Playwright forks for anti-bot bypass?
nodriver avoids the standard CDP automation surface, so on targets that fingerprint the automation protocol itself (rather than navigator properties), it passes where a heavily patched fork is still blocked. In a 7-tool benchmark across 31 protected targets, nodriver was the only tool with zero blocks.What is fingerprint harvesting and why does it matter for scraping in 2026?
How do you scrape a mobile app when the request signature is in native code?
Are AI agents replacing web scrapers in 2026?
Why is my scraper still getting 429 errors after changing proxy provider?
Does using a proxy prevent HTTP/3, and does that matter for scraping?
UDP ASSOCIATE command but browsers do not expose it, so Chrome is TCP-only over SOCKS5 in practice and negotiates HTTP/2 the moment a proxy is configured. It matters because Cloudflare Radar puts likely-bot traffic at 73.75% on HTTP/1.1, 26.76% on HTTP/2 and 3.33% on HTTP/3, so a proxied browser sits on the wrong side of a hardening signal. The options are RFC 9298 UDP proxying (CONNECT-UDP), a network-layer VPN tunnel that carries UDP, or a provider that has implemented HTTP/3 proxying.What is machine identity in web scraping?
Can I use the accessibility tree instead of CSS selectors for scraping?
getByRole and get_by_role. Coverage on the open web is patchy so keep a CSS fallback, but on large commercial sites and anything that has been through a compliance review the role and name are usually present.Can an AI agent write a production web scraper?
Why do some websites serve different content to AI crawlers?
Why can't Claude or ChatGPT reach my page even though it is public?
llms.txt or a markdown version. That makes the search index the gatekeeper rather than your link graph, which for Claude means Brave Search. The design is an exfiltration defence, so it is not going to be relaxed.What is a self-healing scraper and how does it work?
How do you detect which platform or CMS a website is built on?
What is WebMCP and does it replace web scraping?
Where scrapers
talk to each other
The best scraping techniques rarely come from documentation, they come from people who've already hit the same wall you're hitting. These communities are where the real knowledge lives.
Discord servers
Reddit communities
Newsletters worth reading
Free learning resources
Resources from The Web Scraping Club
YouTube channels worth following
Added August 2026 things worth your evening, not just your bookmarks bar
networkidle actually waits for, stops being trivia once you have written the thing yourself. If you only read one long text this year, make it this one.From IP bans
to transformer ML
Every bypass technique was born as a direct response to a specific detection innovation. The escalation explains why each tool exists.
navigator.webdriver=true. playwright-stealth emerges. Playwright 2020, Microsoft, cross-browser. F5 acquires Shape Security for $1 billion.Thank you for reading.
This is everything I know about web scraping in 2026, every detection layer, every anti-bot system,
every library, every architecture I've actually built or used in production over the last seven years.
If even one section saved you a late night of debugging, that's why I wrote it.
Build something interesting with this. And if you do, I'd genuinely love to hear about it.
And if you are stuck rather than shipping, write to me at scrapesync@gmail.com. I answer scraping questions for free, including from people who'll never hire me. It is also what I do for a living, at ScrapeSync, but that's context for who is replying, not a pitch. If you want a pipeline designed, an agentic workflow built, or an architecture review rather than just an opinion, that is the paid version and the same address works.