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
robots.txt itself as a signal before you read it as an obstacle: a site that refuses even its crawler policy file to a declared plain request has usually put a bot wall across the whole domain, and the honest options are a browser that meets its terms, a licence, or a different source. In one re-check of six large UK, French and German retailers, every one returned that 403 from Akamai or Cloudflare. It is not a legal instrument, and it can be geographic: one Cypriot shop reported as open in the survey returned the same 403 to a check from Asia, so test from the region your users are in before you conclude anything.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. Reverse engineering an app or bundle to find its API is a separate legal question from collecting the data, and the answer turns on jurisdiction and purpose. In the EU, Article 6 of the Software Directive (2009/24/EC) lets a person with a right to use a program decompile it without permission only where that is indispensable for the interoperability of an independently created program, the information was not readily available, and only the necessary parts are touched; Article 8 voids contract terms that override it, and Article 5(3) separately allows observing and testing a program while running it. US law differs: 17 U.S.C. 1201(f) permits circumvention for interoperability analysis, yet in Bowers v. Baystate (Fed. Cir. 2003) a licence clause banning reverse engineering was enforced. So decompilation is not simply permitted for any licensed program: in the EU it is tied to interoperability, and in the US an explicit licence term may still bind. This is not legal advice.
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
X-AI response header or JSON-LD. A model must never act on that text. Your pipeline can still log it: store any such directive with the URL and fetch date, the way you keep the robots.txt you fetched, so a person decides deliberately whether that source stays in the corpus. The study counted these by purpose, not by who wrote them, and it is a lower bound covering English, unobfuscated text on crawlable pages. Second, and more pointed for this guide, defenders have stopped pretending robots.txt is the battleground. One practitioner who reverse-engineers scraping tools estimates that on major news brands, while 20 to 30 percent of traffic is identified crawlers, roughly a quarter of all traffic is stealth crawlers mimicking human users, traffic most publishers never even classify as bots.The other direction: sites that hand agents a sanctioned door (WebMCP)
The missing half of the handshake: proving who you are
chatgpt-user.json and searchbot.json, Anthropic publishes claude.com/crawling/bots.json, and Amazon publishes Amazonbot addresses. Treat a request claiming ChatGPT-User or Claude-User as unverified until its source IP matches the operator’s current feed, and never grant such a request more than an anonymous visitor gets. The same rule applies to the dashboards you read, not only to the requests you serve. An analytics tile that says traffic came from OpenAI or Anthropic is only as strong as the signal behind it, and Cloudflare documents that on plans without Bot Management its AI Crawl Control identifies AI crawlers by user-agent string alone; its own API reference calls that filter spoofable and reserves “reliably verified” for Bot Management detection IDs. One publisher who re-checked his dashboard’s OpenAI count against OpenAI’s published feeds reported that only about 22% of roughly 17,850 requests came from OpenAI’s ranges. That is a single site, one screenshot and an unpublished method, and unmatched traffic is unverified rather than proven fake, but it is reason enough to re-derive any AI-crawler figure from raw logs and freshly fetched feeds before you quote it, block on it or bill for it. Match the exact list, not the cloud provider: Anthropic’s own list includes individual Google Cloud addresses, so “came from GCP” proves nothing either way. One site operator reported a single cloud IP rotating five AI-agent user agents while fishing for .env files and receiving 200s where a plain request got a 404; that is an unverified single report, but it shows the failure mode of a user-agent allowlist precisely. The reverse lesson applies if you build scrapers: borrowing an AI crawler’s user agent is disproved by one lookup and puts your traffic in the same bucket as credential probes. For agents, the evidence now suggests declaring yourself is often less detectable than hiding, not just more polite. The same holds for ordinary scripts. Wikimedia’s User-Agent policy says a browser user agent on bot-like traffic “will be assumed malicious”, and in one test Wikipedia answered the library default, a copied Chrome string and a contact-free bot name with 403s while serving both strings that carried an email address. Across 472 homepages in the same single run, a string of the form MyBot/1.0 (https://example.com/bot; you@example.com) httpx/0.28 did as well as Chrome’s, and Uber returned a 406 to every string except that one. A declared identity also gives a site owner someone to contact before they block you. Treat this as one run on permissive homepages, not proof that honesty opens hard targets. Before you add another stealth layer, check that it does not create a new contradiction. A 2026 Inria/Lille honeysite study of LLM web agents found that stealth and undetected modes did not reduce detectability and often raised it: one mode drifted its HTTP headers and reported a non-standard permission value, and another exited from cloud addresses the big vendors had already listed. Combining IP, TLS and browser layers classified visits at 0.993 accuracy across 1,383 visits, from one study on two Linux machines. A separate test found Cloudflare’s free AI-bot controls fully blocked only the one agent that identified itself, so the undeclared agents got through but stayed invisible to site policy. Where your use case can be done openly, a distinct User-Agent token plus signed requests is the stable path. Test from the network you deploy on, because a home connection flatters results a cloud server will not reproduce.Read this before you copy anything from this guide
What changed between complaints. Google dropped the Shopping and Maps theories entirely, did not try to revive the non-copyrighted-content claims, and now pleads four separate sources of copyright-owner authorisation to deploy its anti-bot measure — an unidentified licensing partner, an unidentified content provider, the Reddit agreement, and one more. That pivot exists because the July order dismissed the earlier version for failing to allege that any copyright owner authorised Google to deploy the protection on their behalf.
Why it matters beyond one company. Every anti-bot vendor sells the idea that circumventing their product is itself unlawful. This ruling says that for non-copyrighted material the DMCA does not reach it. Motion to dismiss is Doc. 49, filed 24 Aug 2026; hearing 29 Sept 2026. Not legal advice, and a district-court order binds nobody else — but it is the clearest judicial statement yet on where anti-circumvention stops.
Oct 2026 update. The opposite signal came from New York. On 31 July 2026, in Reddit v. SerpApi (S.D.N.Y.), Judge Engelmayer largely denied dismissal of Reddit’s 1201(a) claims against SerpApi and Perplexity, holding that Google’s bot check plausibly controls access because it treats automated and human visitors differently; only the 1201(b) count and preempted state claims fell. The two orders turn on whether copyrighted material is pleaded behind the measure, not on whether the page is public. Both are preliminary; neither binds the other court.
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.
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.
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 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. Once a session is captured, have the agent summarise endpoints before reading requests one at a time. A HAR file grouped by method, origin and path template, with numeric IDs, UUIDs, long hex strings and ISO dates folded into placeholders, turns hundreds of entries into a short list of candidate APIs, and open-source MCP tooling now does this offline without fetching anything. Treat the result as a map, not a client: such views drop query strings, headers and bodies by design, so the token chain that unlocks each endpoint still has to be traced, and nothing captured in a browser reveals server-side logic.
Frida · mitmproxy
chompjs · Parsel
Scrapy · Scrapling
CloakBrowser
Zyte · Firecrawl
self.__next_f.push) on 33, __NEXT_DATA__ on 29, Nuxt on 13 and Redux-style state on 9. App Router payloads are split across many chunks: join them, decode each as a JSON string, then pull known field names with a regular expression, because the format is internal to React and a dedicated parser failed on the pages tried. Nuxt 3 keeps its data in a __NUXT_DATA__ array of indexes. Those are homepage counts from one run, so check your own target pages.First, place the target on the difficulty ladder
robots.txt, fetch one product page with a plain client, and look for the price in the raw HTML. On 18 European shops named in one practitioner’s 71-site survey, a single re-check found all seven Italian retailers plus Thomann, Otto and Rueducommerce serving prices in plain HTML, while six UK, French and German majors refused the plain request outright. Treat that as one snapshot from one location, not a market rate. When the site is open, take your URL list from the sitemap that robots.txt declares, with its last-modified dates, rather than crawling the navigation; it is cheaper, it tells you what changed, and it is the list the site chose to publish. Expect category and search pages to need a browser more often than product pages do.httpx, the library default got a full page on 353, a copied Chrome string on 375 and a descriptive bot string with contact details on 378, so the browser string bought nothing on average while Wikipedia, W3C and several Meta domains refused it outright. That is one run from one location, on sites already known to accept plain requests, so treat it as a sensible default rather than a guarantee, and check the page you received against what a browser shows.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. Check which problem you have before you escalate at all. A response with no text in it is a JavaScript problem; a challenge page or a 403 is a blocking problem, and a browser is not a guaranteed fix for the second. In one September 2026 run over 544 real homepages from the Tranco top 1,000, a headless Chromium load got past a block that stopped a plain
httpx request on 14% of sites, but was itself blocked on another 5% where the plain request worked, eBay and Zillow among them, and 9% blocked both. A browser is a different identity, not a stronger one. Treat those as homepage figures from one run, one location and one client configuration; deeper pages and other networks can move them. 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.for (;;); — an infinite loop. It is not obfuscation and it is not anti-scraping. It is a defence against JSON hijacking, a flavour of cross-site script inclusion, and knowing why it is there tells you exactly how to handle it.<script src="…/api/…">. The browser fetches it with the victim's cookies and executes the body as JavaScript. Around 2006 to 2009, if the body was a bare JSON array, Firefox let you override the Array constructor or define setters on Object.prototype and read every value as the literal was constructed. That is how Jeremiah Grossman extracted Gmail contact lists in 2006.Why the prefix works. Loaded as a script, the response loops forever before it ever reaches the data. Fetched by the real application, the client strips the first nine bytes and calls
JSON.parse.The practical note for anyone consuming these endpoints. Strip the prefix, do not fight it. And note that a top-level object like
{"payload": …} was never exploitable, because it is a syntax error when loaded as a script — the prefix is applied everywhere because blanket application is cheaper than auditing which endpoints return arrays. Other codebases use )]}', for the same reason. An unfamiliar prefix on a JSON body is usually a security artefact, not an attempt to stop you.Accept: text/markdown instead of Accept: text/html and a growing set of origins return a stripped document with no navigation, no scripts and no styling. Cloudflare shipped this zone-wide as an opt-in feature in February 2026, converting at the edge rather than at origin, free on paid plans.x-markdown-tokens header estimating the token count of the markdown body. For a RAG ingestion pipeline that is a budgeting primitive arriving free in a header you were already reading.It is a one-request probe. Send the header, look for markdown in the response and the token header alongside it. If it is there, you have skipped HTML parsing, boilerplate stripping and script execution in a single move — on a site that decided to hand it to you. Vercel documents the same pattern for Next.js, and several documentation platforms serve it while deliberately keeping the variant out of search indexes.
The caveat that keeps it honest. A February 2026 survey of coding agents found only about three of seven negotiate for markdown at all; the rest still ask for HTML. Support is real but partial on both sides, so treat it as a fast path to try first, never as something to depend on.
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.
/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.
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 ClubWhat 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.Sep 2026 update. A practitioner report on Google Maps (n=1, no code published) follows this recipe: scroll a local-business search, catch the internal batch-RPC call in Fetch/XHR, then strip query parameters one at a time until you find the smallest request that still returns the positional JSON. Claimed ~10× faster than a browser, unmeasured. Scraping Maps is against Google’s terms, and business phone numbers and names can be personal data under GDPR; check before collecting leads.
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. The same contradiction applies when a script with a Python or Go handshake sends a copied Chrome User-Agent: the header names a browser the connection plainly is not. On a single 472-homepage run with httpx, that pairing earned no more full pages than an honest bot string (375 against 378), and it drew 400s from Facebook, WhatsApp and Meta domains and 403s from W3C, MySQL and LG where the honest string was served. If you are not going to make the handshake match, do not let the header claim a browser at all. One run, one vantage point and homepages only, so read the counts as indicative, not as a rate you can plan against.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. Keep the audio surface in proportion. The rendering probe needs no permission and finishes in roughly 5 to 50 ms, so assume it runs on every protected page and that its output has to match the OS and CPU your other signals claim. Real users do not all produce one clean value, though: Safari 17 adds noise in Private Browsing, Brave randomises the output and Tor disables Web Audio, so an unusual hash is weaker evidence than an inconsistent one. The microphone is a separate question. Reading raw input through getUserMedia() with gain control, noise suppression and echo cancellation switched off needs a secure context and an explicit user grant, so it is not a silent anti-bot signal. It matters on liveness and identity-verification flows, where the media stream itself is what gets judged.
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.
Check when your stealth layer last shipped before trusting it.
puppeteer-extra-plugin-stealth is still installed about 835,000 times a week (npm, September 2026), yet its latest release, 2.11.2, dates from 1 March 2023, when Chrome was on version 110, and its repository has not been pushed since July 2024. Every evasion it ships is a JavaScript override of the kind described above, so each Chrome release since then is a surface it has not been updated for, and the overrides themselves are inspectable. If a Puppeteer scraper starts failing, run the same target with the plugin removed before adding more patches. Hosted alternatives move the fingerprint work into the browser build instead: Browserless’s BAP SDK exposes stealth as an endpoint change to /stealth/bql and humanizes typing with a random 50–200 ms per keystroke by default, but it publishes no pass rates or patch list, so treat it as a claim to test on your own targets. 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.localStorage is the strongest link a defender holds: it ties every session from a reused profile together whatever the IP does. Without it, assume hardware is the fallback. One open-source fraud console, OpenFraudMonitoring v1.3, joins a new visit to a known device only when the GPU renderer or device model, the screen geometry, and the CPU count with device memory all match exactly one stored device, and it states outright that IP cannot establish a link. That cuts both ways: identical cloud machines can collapse into a single device, while one changed group splits a real machine in two. It is one project with no published accuracy figure, so treat it as a readable example of the logic, not as how commercial vendors weigh it.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. Run the plain browser on the same targets, headful and headless, before you credit any stealth tool with a pass, and report the control row next to the tool row. A stealth win only counts on the targets where the control fails. In one October 2026 hands-on review of a patched-Firefox tool, vanilla Playwright failed all five vendor-protected sites headless, but headful it loaded the Cloudflare, Akamai, DataDome and PerimeterX sites and failed only Kasada. The tool passed all five, so the evidence that it beats plain automation rests on one Kasada page, tested once from a home residential IP. Most of the headless gap is the headless tell itself, which this section already shows is cheap to remove. Without that control row, a five-for-five table reads as five wins when it may be one.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. Treat a hosted Android persona the same way: check the subsystems it does not list before you trust it on a mobile-gated target. Browserless’s emulationOs=android, documented in October 2026, sets the user agent, Client Hints, a phone viewport and pixel ratio, touch input, motion sensors, battery and network signals, Android system fonts and a mobile GPU identity. Its documentation does not mention CPU architecture, Widevine or PaymentRequest, the very signals in the audit above that only real ARM hardware or a VM fixes, and the sessions run on cloud servers whose architecture is not stated. Probe the relaxed-SIMD oracle, maxTouchPoints and the codec defaults yourself from inside a session. Also spell the value exactly: the documentation says any other value, including macOS in the wrong case, is silently ignored and the session stays Linux.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. Assume a header costume on a non-browser handshake is caught at the edge, before any header value is weighed. Sending Chrome 154’s complete header set, including sec-ch-ua, the sec-fetch-* family and priority, through stock macOS curl 8.7.1 still produced JA4 t13d497h2, 49 cipher suites and 7 extensions, where current Chrome presents t13d1517h2; its HTTP/2 pseudo-headers also arrived as method, scheme, authority, path rather than Chrome’s method, authority, scheme, path. Either difference alone separates the client from the browser it claims to be. That is one measurement from one curl build; other builds and TLS libraries produce other hashes, none of them Chrome’s.: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
eventCounters as an empty [] beside the fingerprint payload, so the vendor sees zero interaction directly instead of inferring it. If a job loads pages without interacting, expect that state to be scored, and prefer a documented data endpoint over rendering a page whose sensor will report an idle visitor. That is one tag build on one site, and how the server weighs the empty field is not public. Botasaurus generates physically realistic curves with randomised velocity profiles.document.querySelector() after DOMContentLoaded looks nothing like a human who reads the page for 2.3 seconds first. LLM agents turn this around. The model’s think time can look human, but the framework underneath still emits machine input, so judge an agent by the events it sends, not by how long it waits. Paste-style or programmatic form filling, keystrokes a few milliseconds apart, and a pointer that jumps straight to its target are all readable. Akamai reported that over 98% of requests from Comet, Atlas and the Claude Chrome extension carried little or no mouse data. An academic study separated seven agents from human participants on behavioural features alone at F1 0.9993, though it was closed-world and dropped to as low as 0.63 on unseen task types. Layering synthetic curves on top gives a model more to check. The cheaper fix is to remove UI actions: when you only need the content, fetch and parse the page instead of clicking through it. Every interaction you skip is one less behavioural signal to score. Warm-up navigation (visiting homepage before target) significantly improves behavioural scores.Runtime.enable appears as a CDP tell throughout this guide, and the mechanism behind the name was narrow: a getter on an Error’s stack that fired when an attached inspector previewed the object, letting a page notice it was watched. Two V8 commits in May 2025 closed it, and the shipped branches settle the milestone: the guard is absent from V8 13.7 and present in 13.8 with no merge-back, which puts it in stable Chrome 138 in June 2025, not the 137 that gets repeated. One read survived on a specification technicality: the guard tests the own property descriptor, and name is never an own property of an error, so the lookup falls through to whatever the page installed on Error.prototype. Hold that lightly. It is read out of shipped source rather than run against a build, the single public measurement is one run on one Chrome build that saw no stack lookups and three name lookups, and the obituary was written by an anti-bot vendor about a method it had just lost. Any attached inspector walks the same path, a developer with DevTools open included, so what survives scores as noise. The startup handshake and the request rhythm above it needed no patch and got none. Not every page-side tell is gone, though, and the live ones key on which protocol domain is enabled rather than on CDP as a whole. Treat the Debugger domain as visible to the page: while it is enabled, V8 writes a private debugging symbol onto the reject callback of Promise.all, so a page that passes a Proxy as that callback gets a TypeError where an empty array should simply resolve. Reproduced on V8 14.6 in Node 26: clean with only Runtime.enable, flipped by Debugger.enable, clean again once it was disabled. Stock Playwright and Puppeteer send Debugger.enable only when collecting JS coverage, so this probe mostly separates instrumented sessions, and probably developers with DevTools open, from ordinary automation. It has not yet been tested in a Chrome build.RTCPeerConnection is using a feature, not checking you. The nearest independent measurement is built differently and lands lower: across the first 10,000 Tranco sites, 3,649 of 7,944 scanned, about 46%, probed a first-tier automation signal, with navigator.webdriver read on 34%. Neither has been replicated. Use the ranking for the one thing it is good for, ordering your effort: the surfaces read before you do anything are introspection, timing and anti-debug, which is precisely the feed a session-long behavioural model is scored from.--remote-debugging-port silently stopped working against your everyday Chrome profile, this is why. From Chrome 136, both --remote-debugging-port and --remote-debugging-pipe are ignored unless you also pass --user-data-dir pointing at a non-standard directory. It is not a bug and there is no flag to restore the old behaviour. A non-default data directory gets a different encryption key, so the profile's cookies and saved passwords stay protected from anything that talks to the debug port.Storage.getCookies returns the cookie jar without ever touching the encrypted store. The 136 change closes the convenient version of that: you can still enable CDP, but not against the profile that holds the sessions.What it means for your setup. The attach-to-my-real-logged-in-browser pattern is over on default profiles. Pair the flag with a throwaway
--user-data-dir and you get a clean profile that you must authenticate into — which is the point. Plan for that in any workflow that assumed an existing session, and note it applies to Playwright and Puppeteer identically, because the restriction lives in Chrome, not the driver.If you run the endpoints being scraped. Researchers have since shown the restriction can be sidestepped by enabling CDP from inside an already-running process rather than via command line, which no browser flag can prevent. The published detection is host-level, not browser-level: Sysmon Event 8 (CreateRemoteThread) and Event 10 (ProcessAccess) targeting
chrome.exe or msedge.exe, with access mask 0x143a the notable one. Worth handing to whoever owns endpoint detection, because a stolen live session walks straight past multi-factor authentication — the authentication already happened.Why it belongs in this guide, from both sides. If you build collection, this is the anti-pattern that gets a whole technique class blocked for everyone: cache-busting every request is maximally hostile to the target's economics, so it draws the hardest possible response and trains defenders to watch for per-SKU polling cadence. If you defend, the lesson is that a bot share alone tells you little — 551 polls at a fixed 6.5-second interval with cache-busters is a signature you can write a rule against, where a raw percentage is not. It pairs with the guide's cost material from the defender's chair: this traffic is expensive precisely because it is engineered to bypass the cache and hit origin every time.
Source: Tom's Hardware, on DataDome research by Jerome Segura.
--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.
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.
Oct 2026 update. A bot-management vendor’s staff made the defender’s version of this point: a
curl call carrying Chrome 154’s full header set should be caught by any protection worth paying for. Checked here with that exact command: macOS curl 8.7.1 presented JA4 t13d497h2 (49 ciphers, 7 extensions) against Chrome 154’s t13d1517h2, and HTTP/2 pseudo-headers as m,s,a,p where Chrome sends m,a,s,p. One curl build; vendor-authored post.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.
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
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.
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
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
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.hyphens: 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/joe12387curl_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.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 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.
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.
Check whether anything is protecting it at all. Do not assume a hardened target by default. In a June 2026 outside-in test by DataDome, ten simulated bot profiles sent from residential IPs to 21,491 popular homepages were stopped by none of the defences on 65.3% of sites; only 2.4% stopped or challenged every profile, bots openly impersonating AI services got through on over 70%, and only 5.5% caught forged fingerprints. So send one plain request first and read the response before reaching for a browser, a residential pool or a solver. Two limits apply: the test only touched homepages, and many sites leave the front page open while protecting login, search and checkout, so repeat the check on the page you actually need. And the figures come from a bot-protection vendor describing the market it sells into, with the bot profiles and pass rule not published in detail, so treat them as a rough base rate rather than a census.
captcha-delivery.com, or the challenge page itself. Read the 403 body before you touch the fingerprint. On leboncoin.fr in September 2026, four curl requests with and without a Chrome User-Agent all drew a 771-to-774-byte challenge bootstrap, and the User-Agent changed only the bootstrap type (rt=c or rt=i). A different challenge is the same refusal, not progress. The sensor host can also look like the site’s own (dd.leboncoin.fr, served from S3 behind CloudFront), so a vendor check that only looks for third-party script domains can miss it. That is one site and one tag build, and the server-side scoring is not visible from outside. PerimeterX (HUMAN) leans heavily on behavioural signals, so a perfect static fingerprint still fails without human-like interaction over time. And Akamai is deep TLS and HTTP/2 fingerprinting, where pseudo-header order and JA4-level detail decide the outcome. These are tendencies, not laws, and every vendor blends all the layers, but knowing each one's centre of gravity keeps you from spending an afternoon hardening the layer that particular wall was never watching. Hold that map loosely: what you identify is a behaviour on your fetch path, not a table entry, and flatly stated rules soften the moment somebody runs a control. Google’s wrapped result links are widely described as a tax every Chrome pays; one fresh, hand-driven profile on one query returned 213 direct links and zero wrappers, while the same profile with an unattached --remote-debugging-port returned 96 wrapped and none direct. Two trials against one control, no mechanism identified, a link count nearly halved between arms — enough to retire the word always, not to install a replacement. Count wrapped against direct under your own launch config before budgeting a request per result, then read what you were already handed: on one sampled page the destination still sat beside the wrapped token in the page’s own server-rendered state, pairing 8 of 8 web results at no extra cost — one capture, denominators of seven to nine, reported mostly by firms selling SERP APIs.Then hold something back even when a fetch comes back clean: a
200 is not an identification. Traffic-scoring products sold to advertisers route traffic they distrust to a honeypot, and name antivirus scanners by brand while explaining their own false positives; joining those two facts is an inference, not a marketed feature. The catalogued variant that matters for recon withholds payloads from proxy, VPN and datacentre addresses as likely researchers. A datacentre exit is not refused there, it is made blind. Re-sample from another address class, and at another hour, before closing the case.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 layerGPTBot or ChatGPT-User is counted as that crawler and inherits whatever rule the owner set for it, very often a flat block. On one publisher’s site the dashboard attributed about 17,850 requests to OpenAI, about 11,890 of them were answered 403, and only around 22% came from OpenAI’s published ranges (one site, method unpublished). Two consequences for you: a borrowed AI user agent buys no access on a site that blocks that crawler, and it puts you in a bucket the owner can split from the real operator with a single IP lookup. An honest user agent of your own is the cheaper position.geoip=True, 100% pass rate Mar 2026 on Instagram, Reddit, X, LinkedIn (FF135 base; stable moved to FF146 on 16 Jul 2026 — re-test)StealthyFetcher 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 country403 as a top-level navigation and 200 as the page’s own same-site fetch (Origin + sec-fetch-mode: cors) — empty cookie jar, no token ever issued. One endpoint on one site, self-reported; rules are per customer and path(Linux) token blocked. Bracket every A/B with a known-good control: a failed burst degrades the exit IP for ~15 minutes and then everything fails_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.reese84 is earned by a browser sensor, and on at least one deployment it was not tied to the client that earned it. Warm a session once in a real browser, save the whole jar (reese84 plus the incap_* and nlbi cookies), then replay it from a plain TLS-impersonating client on a different network. If the real page comes back, you can split the work: pay for the stealthy warm-up, and spend the token from cheap infrastructure until the server’s renewInSec runs out. In a September 2026 test on one retail site, a jar with renewInSec of 699 seconds (about 11.5 minutes) returned full HTML from a residential IP, a datacenter IP and a home line in another country. A captcha-assisted warm-up cost around a tenth of a cent. That is one site on one date, and binding can differ per deployment, so treat it as a test to run, not a rule to assume. Bypass: residential proxies (datacenter IPs nuked instantly), Camoufox or fortified browser, slow request pacing.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 ___utmvc cookie | Imperva (Incapsula) block | reese84 | Cookie names · Incapsula challenge script |
TS* or TSPD_101* cookie | F5 (BIG-IP / Shape) block | TSPD_101* | 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 |
j-1.2.726: p.js is 163,120 bytes around a 136,736-character string blob, decoded by a variable-radix varint with a 72-character alphabet at radix 48; ips.js is 655,603 bytes around a 603,415-character blob, alphabet 71, radix 46, and wrapped in a wall-clock time lock.p.js, which fires an mfc request and creates two iframe navigations to an fp endpoint. The fp response is a nine-line HTML bootstrap, and it is that bootstrap, not p.js, that creates window.KPSDK — pinning KPSDK.now to performance.now bound to performance with Date.now as fallback, stamping KPSDK.start immediately, then posting to the parent. Only then does ips.js load, carrying three query parameters: KP_UIDz, x-kpsdk-v (the SDK version, in cleartext) and x-kpsdk-im.Four layers, and only one is worth your time. p.js is the loader plus integrity checks. ips.js is the fingerprinting program. Then the transport, then the server. The researcher went for ips.js and ignored the rest.
The methodological line worth stealing. He graded every finding by evidence strength, because a fingerprinting inventory built from decoded strings is mostly wrong: “A string sitting in a table hasn't been read by anything.” Finding
navigator.hardwareConcurrency in a decoded string table proves the string exists, not that the program reads it. Distinguishing the two is the entire difficulty of the exercise, and it is the same mistake people make when they grep a bundle and announce what a vendor checks.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.
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.
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 teardownThe 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 (Zyte), full report
iamNotaRobot.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."requests 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/anubisWhy 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.
+= 8 turns into a string, so the real interpreter rereads byte zero while a version that “tidies” the function to 0 reads the next byte, and both reportedly end at the same position. The coercion itself reproduces in Node 26: bytes AA BB come back AA AA from the function-valued register. That account is a single practitioner’s, with no published build, so treat the specific trick as illustrative; the testing rule holds for any VM-protected sensor. And before spending weeks on it, check whether the data has an official API, which for most Google properties will be the cheaper route. Application-layer detection alone means shipping obfuscated challenge code inside a custom VM and staying ahead of the people who devirtualise it for a living, knowing which canvas and timing artifacts separate real Chrome from automation and which an attacker can cheaply fake. Below it sit entirely separate specialisms: TLS fingerprinting, HTTP/2 frame ordering, reading the OS and tunnelling from raw TCP. Real detection holds all of them current at once against an adversary who ships patches in hours, and the moment a vendor leans on a single layer, that is the layer that breaks. The practical consequence for a scraper is the inverse of the marketing: a great deal of deployed protection was built once and quietly stopped being touched. A toggle implies the problem is solved and static; it is neither. When a defence feels immovable, the useful question is how recently anyone actually worked on it.
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. Check your impersonation profile against the current Chrome release, not against the version it was named after, because one missing extension is enough to move your JA4. In an independent twice-weekly measurement on 26 Sep 2026, Chrome 154 through 156 all sent 17 ClientHello extensions, including the new trust_anchors (0xca34). tls-client with its chrome_152 profile matched on JA4, groups, GREASE and HTTP/2; curl_cffi 0.16.3 with chrome150 sent 16, lacked GREASE in signature algorithms, and so failed JA4 while still passing groups and HTTP/2; uTLS’s auto Chrome profile also failed HTTP/2. Treat these as one young tracker’s macOS results covering only the handshake layer, so a pass is necessary rather than sufficient, and re-run the same comparison yourself whenever a new Chrome stable ships. 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.
bogdanfinn/tls-client with a Chrome profile and randomised extension order; (2) header order, not just which headers but the exact sequence including the HTTP/2 pseudo-header order; and (3) a valid DataDome token minted from real sensor data, which is the one part you cannot fake by copying headers. Get the first two wrong and you're flagged before the server reads a single value.The detail that proves they actually did the work: the
bck.hermes.com API sends every header lowercase over HTTP/2, while DataDome's own geo.captcha-delivery.com endpoints run HTTP/1.1 and want Chrome's Title-Case names. They matched every request against a real browser capture to get that right. Header order was declared explicitly in code, never left to a map's iteration order.Why no browser, in one number. The product page is just a wrapper around
bck.hermes.com/product?productsku=..., which returns the structured object directly — price, every colour and finish variant, breadcrumbs, stock, dimensions — at about 37 KB per product. The equivalent full page render is roughly a hundred times heavier, and you pay residential proxy rates on every megabyte of it. This is the cost section's argument made concrete: the bill scales with kilobytes, not with a browser farm.The flow worth stealing. Warm one session through DataDome once, reuse it for dozens of calls: load the homepage, solve the challenge if it fires, post the client tags (
ch then le) to raise the trust score, call /sync-form to mint the x-xsrf-token, then hit the API — rotating the session after a set number of calls so no single datadome cookie is stretched too far. And you don't need a sitemap: bck.hermes.com/menu exposes the full category tree, and a top-level category already aggregates its whole sub-tree (pulling WOMEN returned all 2,785 women's products at pagesize=144).The honest caveat is that the metered part — the DataDome solves — runs through a paid API (Hyper Solutions), so this isn't free; it's cheap because the sensor calls amortise across every product a warm session pulls, and everything else is plain HTTP. That's the real lesson, and it's the opposite of the managed-challenge dead end elsewhere in this feed: where the token is the product and the session is reusable, browserless collection is dramatically cheaper. Where the challenge grades the browser every time, it isn't. Knowing which case you're in is the whole skill.
Source: Hyper Solutions guest post on The Web Scraping Club; repo open-sourced.
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. Treat any star count in the table below as a dated snapshot, not a live signal, and check a repo’s last push, licence and archived flag against the GitHub API before you adopt it. A star count can now move faster than anyone could read the code behind it, so weigh it least of all the signals. One reverse-engineering MCP server went from 136,505 stars on 12 October 2026 to 260,901 on 15 October, nearly doubling in three days, while its own commits celebrated each milestone and its stargazer list could not be enumerated to see who starred. The curve may be genuine attention, but nothing public lets you audit it. Before adopting a fast-rising tool, read its releases and security notes, pin an exact version, install with lifecycle scripts disabled, and try it offline on traffic you captured yourself. For a tool that reads apps and pages on your behalf, check what it keeps and what it passes to your model provider; no star count answers either question. A 28 Sep 2026 recheck of seven popular scraping repos found every one alive and none relicensed, but the table’s own figures had drifted badly: Scrapling was listed at 38k against a live 84.2k, ScrapeGraphAI at 18k against 31.4k, Crawlee at 15k against 25.9k and Katana at 8k against 17.6k. One widely recommended research tool, minet, has no row, and it ships under GPL-3.0, the only copyleft licence of the set, which matters if you embed it rather than run it. Stars measure attention, not whether a tool clears a given anti-bot, so use them to spot abandoned projects rather than to rank capable ones. Read the licence file and the commit graph as well as the star count, because a young stealth tool can be very popular and still rest on one maintainer. invisible_playwright had 3,009 stars less than five months after its 13 May 2026 creation, but one account wrote 663 of its 728 commits, PyPI shows 38 releases in the 30 days to 5 October, and the engine ships only for Windows and Linux. GitHub lists its licence as MIT, while its own pyproject.toml declares MIT AND Apache-2.0 because it bundles a fork of Playwright’s Python client, and the patched Firefox binary is MPL-2.0. Pin an exact version, keep the engine build you validated, and check the declared licence in the package metadata rather than the repository badge if you redistribute it.
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 91 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 | 84k | |||
|
⚡ 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 | 26k | |||
|
🌐 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 | 31k | |||
|
⚡ 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
|
||||||||
| CapMonster Cloud ⚡ | CAPTCHA | API | ~$0.60/1k v2 | reCAPTCHA, Turnstile, image | – | |||
|
⚡ HTTP
Under the hood: AI solver. Hosted sibling of the downloadable CapMonster app; a model answers, no humans in the loop.
✓ Pros
✗ Cons
|
||||||||
| NopeCHA ⚡ | CAPTCHA | API | sub, daily caps | reCAPTCHA, hCaptcha, Turnstile, text | – | |||
|
⚡ HTTP
Under the hood: AI solver sold as a SUBSCRIPTION with daily solve caps, not per-solve — so effective cost is a utilisation question.
✓ Pros
✗ Cons
|
||||||||
| DeathByCaptcha ⚡ | CAPTCHA | API | discount tier | reCAPTCHA, image, text | – | |||
|
⚡ HTTP
Under the hood: Hybrid: a model tries first and a human worker picks up the miss. One of the older discount solvers.
✓ Pros
✗ Cons
|
||||||||
| SolveCaptcha ⚡ | CAPTCHA | API | per-solve | reCAPTCHA, hCaptcha, image | – | |||
|
⚡ HTTP
Under the hood: Automatic recognition service, API-compatible with the 2captcha family. Marketed as automatic; no explicit human-vs-AI split stated by the vendor.
✓ Pros
✗ Cons
|
||||||||
| CapSkip ⚡ | CAPTCHA | Local API | flat licence | reCAPTCHA, Turnstile (local) | – | |||
|
⚡ HTTP
Under the hood: LOCAL AI solver: the model runs on YOUR machine and exposes a local API that emulates the 2captcha / RuCaptcha / SolveCaptcha endpoints. Flat licence, no per-solve fee.
✓ 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 | 18k | |||
|
⚡ 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 DISCONTINUED | Framework | Node.js | Proxy manager | Discontinued Feb 2026 | 2.4k | |||
|
⚡ HTTP
Under the hood: Discontinued 6 February 2026 after eleven years — kept here because it is still widely recommended. Was a self-hosted proxy pool manager that provisioned proxies on AWS, Azure and GCP and rotated IPs automatically. Docker images have been pulled from public registries, the documentation is offline, and shared infrastructure is shut down for non-paying users.
✓ 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. When you judge a stealth tool, ask what it changes in the engine’s own bookkeeping, not only which JavaScript values it rewrites. Patching navigator properties or console serialisation leaves untouched the code paths V8 alters while a debugger is attached, and those are readable from ordinary page code: with the Debugger domain enabled, an empty Promise.all given a Proxy reject callback throws a TypeError instead of resolving (reproduced on V8 14.6). A fingerprint benchmark that scores JavaScript values will not show that difference. It is one vector, shown by one researcher and reproduced outside Chrome, so read it as a reason to audit which protocol domains your stack enables rather than as proof that vendors check it.HTMLScriptElement.supports and DOMTokenList.supports — were enough to drop one Shopify theme’s loader onto fetch()-based chunk loading and re-pull the same Vue bundle nine times a page against Chrome’s once: 676 requests to headless Chrome’s 432 over two pages, warm runs about 46% slower than the browser being replaced. Read that as mechanism, not magnitude — it is Lightpanda’s own harness grading Lightpanda, one storefront, reported medians, no independent replication. Both gaps are patched; the next will be a different API on a different theme. So qualify a light engine the way you qualify a proxy pool: diff request counts against a real Chrome on your own targets, and read the defaults while you are there. Diff the output as well as the traffic. On a demo page that builds its list with document.write(), jsdom returned all 10 items in exactly the reverse of Chromium’s order, with no error and nothing missing, which no memory or success-rate benchmark would catch and which corrupts any record where position is data, such as rankings and search results. Compare word counts and the order of a few known items against a real browser for each target before you switch, and repeat the check after an engine upgrade. That is one page and one engine version, so read it as a class of failure to test for rather than a measured rate. That same engine installed no HTTP cache unless you passed a cache directory, and the flag alone bought more than the API fix.
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 botasaurusNOASSERTION, the PyPI record carries a null licence field and no License :: classifier, while the wheel still ships the file and repeats the terms in the installed package’s own docstring. An SPDX-keyed gate sees a blank and waves it through, so grep your lockfiles for the name instead. None of this is legal advice — “commercial purposes” is undefined, automatic expiry is a unilateral term of untested strength, and no enforcement is on record — and note that the library’s speed advantage over Selenium is its author’s claim about his own project, with no published benchmark behind it.
from 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])
scrapy-stealth release adds driver="auto", and it is this guide's cheapest-first principle turned into a runtime decision. The engine attempts fast HTTP impersonation first and escalates to a real Chrome browser only when it meets a JavaScript challenge or a ban — so you pay browser prices on the pages that need a browser and request prices on the ones that do not.The rest of the release, briefly: proxy health scoring with automatic cooldown on dead exits; behavioural mouse and scroll replay with no configuration; browser cookie handoff, so you log in once in a browser and reuse that session on the faster request path; custom DNS overrides to pin hosts to fixed IPs; and static-asset blocking to skip images, fonts and CSS on browser fetches.
The one to actually look at is the cookie handoff, because it attacks the usual reason people run browsers at all. Very often the browser is not needed to render anything — it is needed once, to acquire a session. Separate those two jobs and the browser becomes a login step rather than a per-page cost.
difflib.SequenceMatcher from the Python standard library: Ratcliff/Obershelp gestalt pattern matching, imported on line 4 of parser.py.0.5 × key-sequence ratio + 0.5 × value-sequence ratio — then normalises to round((score / checks) * 100, 2).Three consequences worth knowing before you trust it in production. First, the default threshold is
percentage=40, so an element scoring 40% similarity is accepted as a match. Second, relocate() deliberately never stops early even on a perfect score, so it is O(n) across every node on the page, every time. Third, it returns a list, not a single element, so tied candidates all come back.This is not a criticism. String diffing over a rich property dictionary is a sensible, debuggable, dependency-free design, and it is why the library is fast and installs cleanly. It is a criticism of reading “intelligent” in a README and assuming a model is involved. Know the threshold, and set it yourself.
md.convert("https://example.com"). Under the hood that is a plain HTTP GET through Requests — no browser, no fingerprint management, no retries. Against anything with anti-bot protection it simply fails. Scrape with your own stack and hand MarkItDown the saved HTML.Two more. There is no proxy option at all; because it uses Requests, you configure
HTTP_PROXY and HTTPS_PROXY as environment variables before instantiating. And YouTube conversion typically returns 429 Too Many Requests even from a fresh IP, so that feature is unusable at scale without paid residential infrastructure — a cost the docs do not mention.Practical setup. Python 3.10 or higher.
pip install 'markitdown[all]' pulls heavy dependencies, so prefer selective extras — there are eleven, including [pdf], [docx], [audio-transcription] and Azure Document Intelligence — and keep the scraper container light. Plugins are disabled by default and must be enabled explicitly.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.
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.
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.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.
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. If you are building a corpus rather than a price feed, do not spend long choosing between heuristic extractors. A September 2026 CMU preprint ran four of them (resiliparse, trafilatura, jusText and the model-based Dripper), each followed by the same RefinedWeb rules, and at 1.4B parameters all four landed within 0.003 DCLM Core of each other. All four trailed a single 0.6B model (ReScraper, Apache-2.0) that reads the whole rendered page and decides what to cut. The input mattered more than the extractor: heuristic extraction fused table cells and dropped bulleted lists before any cleaner saw the text, and no later stage could put them back. So keep the raw HTML, treat the extractor as replaceable, and test extraction on your own tables and lists, not only on article prose. This is one preprint on one Common Crawl pool, scored by downstream pretraining rather than extraction accuracy, and nobody has reproduced it yet.
Describe, don't
select
AI-native scraping replaces CSS selectors with natural language. A 2025 NEXT-EVAL benchmark showed an LLM hit F1 0.96 on record extraction when the page was fed as flattened JSON, and 0.10 when fed as slimmed HTML: the input format, not the model, decided the result. The 0.96 (exactly 0.9567) is Gemini 2.5 Pro on 164 real pages after each page was flattened into a map from XPath to text. Given the same pages as slimmed HTML it scored an F1 of 0.10 with a hallucination rate of 0.91, and on nested JSON 0.40. So convert the DOM into a flat, addressed representation before you ask a model for records, and have the model return paths that code then applies. Do not quote the 0.95 as what an LLM does on HTML. It is one model on one benchmark from 2025, and the authors designed both the benchmark and the input formats.
/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)npx create-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) # NEXT-EVAL 2025: F1 0.96 on flat-JSON input, 0.10 on slimmed HTML
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. Keep the JSON-LD, but never hand it to a model as trusted text. Structured data is where scrapers go for clean fields, and it is also a delivery channel for instructions aimed at them: a 2026 scan of 1.2 billion URLs found 1,996 validated prompt injections in structured data, 26% of those in page bodies and mostly JSON-LD, plus 7,887 in custom response headers such as X-AI. Parse those fields with code, screen headers and JSON-LD for instruction-like text before any model call, and do not make plain text the default model input. In the same study’s 5,200 trials, a page flattened to plain text was obeyed 3.9% of the time against 1.1% for HTML, because stripping the markup also strips the evidence that the text was hidden. Read that gap as indicative rather than exact, since HTML inputs also overflowed smaller models’ context windows in about a fifth of runs.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. A diagnosis only runs on rows you decided to look at, and that decision is usually delegated to the model’s own confidence score — the one number in the pipeline produced by the thing under audit. Treat self-graded confidence as routing metadata, not as evidence, and the case for sampling thins the moment you price a separate checker. A purpose-built, non-generative extraction judge such as TypeSafe’s jev line lists at $0.042 per million input tokens against $0.75 for the mini-tier extractor it audits, output unmetered — about 18× cheaper per input token on published September 2026 rate cards. That is arithmetic on list prices, not a measured per-row saving; one gateway is reported to be serving the judge free on promotion, so a $0 metering line is a campaign, not a price; and the accuracy chart beside it is a vendor grading its own product against LLM-written ground truth. What the ratio changes is the default: you check every field rather than the one row in eight a confidence gate forwards, and your escalation rate sets the bill, because the rung you escalate to meters reasoning as output at $30 per million. Two design points outlive the pricing. Score each field on several independent binary failure modes — fabricated, off-target, wrong type, malformed, incomplete — and aggregate them with max, never a mean, so one confident flag escalates instead of averaging into silence. And keep the scope honest: a same-bytes check proves fidelity to the page you fetched, never that the page was fresh, unblocked, or truthful. That still costs a re-fetch.
Serving scraped data to an agent: the naming problem replaces the scraping problem
rm 1-18 or an exact substring to delete, and plain code applies them, so every kept word is copied from the page rather than retyped. On its held-out pages, 96% lost a leading block and 98% a trailing one, and those edge blocks held 89% of the removed words: boilerplate sits at the edges. The output grows with the number of removals, not with page length, which is what keeps a 0.6B model cheap. Fidelity drops on the one operation that writes new text, rewrite (mean BERTScore 0.89 against the source), so switch it off for data that must stay verbatim. This is a research preprint aimed at pretraining corpora, not a validated tool for record extraction. If your corpus starts from Common Crawl, do not take its ready-made WET text as it is: extract from the WARC HTML instead. On 664 English pages from one 2026 WARC file, FineWeb’s own quality filter passed 10% of WET pages against 51% for trafilatura and 34% for Resiliparse, and WET kept about half as many usable words, losing mostly to menus and link lists. Between the two extractors, the DataComp-LM paper found similar downstream model quality and an 8× speed gap, and the retest measured about 17× on one machine, so Resiliparse suits bulk work and trafilatura suits smaller jobs where each page counts. That filter was tuned on trafilatura output, which may favour it, so keep the raw HTML and treat the extractor as a choice you can rerun. Automated hidden-API discovery: rather than hunting XHR calls by hand in DevTools, load the page once in a real browser, record every fetch it makes, and return the JSON-ish endpoints, turning the guide's find-the-backend-API advice into a repeatable step. Change tracking: diff a page's main content against its previous scrape and return a status of new, same, or changed with the added and removed lines, which is the cheap way to watch for the site drift that breaks extractors before it silently corrupts your data.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. Never let the agent that writes a scraper fix also be what decides the fix is right, and never let it see the answers. In one open-source self-healing Scrapy prototype shown at Extract Summit 2026, the repair agent, working in the live repository, found the test site’s templates, a README naming the expected outcome and an API listing the ground-truth endpoints, so its workspace had to be cut down to a one-commit copy of its spider. A variant it wrote for pages it had never been shown took a Prev link for Next and delivered 445 of 566 items with no error, until the gate required held examples and page numbers that go up by one. Zyte’s CEO described the same pattern in production: an agent that added “green tea” to a list of non-colours to satisfy one failing test. Curate what the agent can read, validate on held pages it did not choose, and treat a special case added per failing test as a smell. These are one prototype’s logs and one talk, not rates. 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. Host allowlists on anything that carries a credential; authentication on any local approval server; a size cap on anything that writes to disk. Apply the same rule to scraping middleware that runs a local proxy relay. Any helper that adds your upstream
Proxy-Authorization to forwarded traffic has to listen on loopback or check who connects. Otherwise it is an open proxy billed to your account. In scrapy-stealth 1.0.0, turning on the new STEALTH_CDP_URL mode moves that relay from 127.0.0.1 to 0.0.0.0, still with no client authentication. On a cloud VM with a permissive security group, that can put your residential credentials in reach of anyone who finds the ephemeral port. This comes from reading the source, not from a demonstrated exploit, but the check is cheap. Before running any stealth layer, grep it for start_server, 0.0.0.0 and what it writes into site-packages or sends home at startup. Then run ss -ltnp during a crawl to see what is actually listening. If you need a remote browser, put it behind a firewall rule or an SSH tunnel. 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.
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.
Freshness. 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
Oct 2026 update. A follow-up Web Scraping Club piece (Federico Trotta, 13 Sep) shows the search-integration risk in chat. Claude, ChatGPT and Perplexity ran different queries and cited different sources for the same prompt, and changing “tell me” to “find” changed Claude’s second query. Given a paywalled URL, Claude said it could not open it and answered from “other coverage”; ChatGPT and Perplexity did not flag the failure. Screenshots only, no counts or repeats.
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
pip install cocoindexconfigure sources (files, URLs, S3), define your chunker and embedding model, run cocoindex.build()done in under 10 minutes.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.
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.
If you let a coding agent drive a browser through a thin CDP harness that writes its own helpers, budget for the harness to grow, and keep the fetch-first rule. Browser Use’s Browser Harness was launched as roughly 600 lines across run.py, helpers.py and daemon.py. Five months later the same three files total 1,975 lines, and the repo also has auth, MCP and recording modules and about 97 domain-skill folders. So the claim that you get a small core the agent extends holds only partly: the vendor’s core keeps growing as well. Treat agent-written helpers as code you review before reuse. The published cases, such as an upload helper that moved to chunking after a 10 MB websocket limit, come from the vendor and give no success rates. A single local daemon has one current tab, so run parallel agents through separate browsers or queue them one at a time. The harness’s own skill file gives the same rule as this guide: use curl for a public page, and open the browser only when that fails or returns an empty shell.
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. Keep the scope of an agent’s browser out of the model’s reach. If the agent can pass its own allowed-domains list or browser-session name as tool arguments, a page that injects instructions can widen both. One open-source research workbench that drives the user’s real, logged-in Chrome strips those arguments from every model-issued browser call and sets them from the application’s settings and the conversation’s identity instead, so a prompt cannot reach another conversation’s browser. That is the shape to copy: the model chooses what to click, and the application decides where it may go and whose session it acts in. Check that such a guard is actually loaded, though: the same project’s source records an earlier version in which the guard module failed to register and was silently never applied. 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. Keep the scope of an agent’s browser out of the model’s reach. If the agent can pass its own allowed-domains list or browser-session name as tool arguments, a page that injects instructions can widen both. One open-source research workbench that drives the user’s real, logged-in Chrome strips those arguments from every model-issued browser call and sets them from the application’s settings and the conversation’s identity instead, so a prompt cannot reach another conversation’s browser. That is the shape to copy: the model chooses what to click, and the application decides where it may go and whose session it acts in. Check that such a guard is actually loaded, though: the same project’s source records an earlier version in which the guard module failed to register and was silently never applied. 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. Build on it knowing the money behind it. Verified rounds from May to September 2026 alone include Exa ($250M at $2.2B), Oxylabs ($130M at about $3.6B, its first outside capital), Parallel Web Systems ($100M at $2B) and Firecrawl ($75M), and lifetime totals across 13 agent web-access companies reach roughly $1.3B. That capital buys below-cost pricing today and makes consolidation or price rises likely later. It also pushes infrastructure vendors up-stack: a proxy or scraping supplier with fresh capital may launch the same agent search API you are building on its network. Keep your fetch and search providers behind an interface you can swap, and do not size a business case on today’s per-call prices. When you compare search-and-extract providers for an agent, split the score in two: what each search surfaced, and how each extractor did on the URLs both returned. In one vendor-run comparison of Apify’s RAG Web Browser and Jina AI Search on a single query, the headline was 8 usable results against 6 and one access failure against three, yet the six sources both tools returned were usable on both, and all three of Jina’s failures (403s and a 401 from Forbes, Reddit and Reuters) were pages the other tool never returned. A blocked publisher in the result list is a discovery outcome, not an extraction defect, and a 47% larger text haul is also a larger token bill for the model that reads it. Run several of your own queries on the topics your agent actually serves, and define “usable” before you look at the results. When a search API can answer for you, switch that off and ask for the page text instead: request raw content, keep the URL beside it, and run your own model over it, so the corpus you audit is the one your agent actually read. Check the vendor’s defaults before trusting a wrapper’s promise. On Tavily, the generated answer is already off by default and raw content is off too, so raw content is the one flag that changes what you receive; the snippet field is still generated server-side according to search depth, and the Research endpoint returns a report written by Tavily’s own model. A client that advertises “no AI post-processing” can only vouch for its own side of the wire, never for the vendor’s. Widely shared “$1B in 12 months” tallies mix older rounds with lifetime totals, so check dates before quoting them. 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. When you choose a scraping MCP server for an agent, count the tools it registers before you compare anything else, and load only a subset where the server allows it. One vendor comparison of nine scraping MCP servers, checked against documentation rather than run, lists about 7 tools for Jina, 9 for Crawlbase, 26 for Firecrawl’s full profile and 28 for ScraperAPI. That is a fourfold spread in what rides along on every turn before a single page is fetched. Bright Data exposes GROUPS and TOOLS settings so you load only what you call. The counts come from vendor documentation and shift between versions, and nobody has published a token cost per server, so measure your own system prompt with each candidate installed. Measured counts for a few keyless servers now exist, so use them as a floor and then check your own build. Counted with one OpenAI tokenizer on 12 October 2026, Playwright MCP 0.0.83 declared 25 tools in 4,450 tokens and Chrome DevTools MCP 1.10.1 declared 30 in 6,527, well below the older figures quoted below for the same servers, and six servers together came to 15,950. The same run found that one Playwright snapshot of a single Wikipedia article cost 31,565 tokens, so for a scraping agent the pages it reads outweigh the menu within a call or two. Treat these as one run on one tokenizer without any client wrapper, and re-measure after every server upgrade, because tool menus change between releases. A crawler that only ever calls one fetch tool gains nothing from a 26-tool menu, and it pays for that menu on every step.
scrape(url) → markdown tool has nothing there to cut. Its bill is the payload it drags back. Trim that first, because it is free, and the menu second, because it costs you a sandbox.
Promise.all (reproduced on V8 14.6). The same server’s documentation warns that precise coverage changes timing and JIT behaviour. Run reconnaissance in a separate profile and session from the crawler that follows, so whatever the target learns about the inspection is not attached to the identity you later scrape with.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
402 Payment Required where it used to meet a block. Cloudflare’s Monetization Gateway, announced in July 2026 and so far only a waitlist, would let a site charge any caller for a page, an API route or an MCP tool call over x402, settling in stablecoins with no account: the server states the price, the client pays and resends with proof, and a facilitator verifies. Cloudflare’s examples run from $0.01 per call on a premium route to variable prices up to $2, and include turning an origin’s 401 into a 402. If you give an agent a wallet, give it a per-task budget, an allowlist of payees and a log of every payment it makes, because a page can now ask for money as easily as it asks for a click, and content the agent reads may try to steer that decision.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. Agents got the harder default, not the softer one. Cloudflare’s 15 September presets for a new ad-supported domain allow Search, set Training to Disallow AI Training (a robots.txt preference that Accountable crawlers honour), and set Agent to Block on pages with ads, a network block decided page by page. Cloudflare says it offers no Disallow setting for agents yet, because no established directive exists to express one. So an agent acting for a person cannot earn access to those pages by reading and obeying robots.txt; it is refused at the edge on exactly the pages that carry ads, while the same site’s ad-free pages stay open. If your agent’s success rate splits along that line, suspect the ads detector before your fingerprint. This is Cloudflare’s description of its own presets, and owners can change them at onboarding or any time afterwards.The metric worth borrowing. They introduce a Weighted Efficiency Score that penalises both inefficiency and the wasted steps a failure drags behind it. The gap it exposes is the point: the best agent with public trajectories scores 41.4% on success but 15.6% on WES. Success rate flatters agents enormously, because it counts the finish and ignores the wandering.
What this means for a scraping budget. Per-call browser-infrastructure pricing is the number people compare, but steps-per-task is the multiplier nobody prices in. An agent that needs three to four times the necessary actions pays that multiple on every one of them. It is the strongest available argument for spending the model once at build time to generate a deterministic extractor, and running the extractor per page.
@cloudflare/computer, announced 3 Aug 2026, is a virtual filesystem living inside a Durable Object with authoritative state in SQLite, exposing one pluggable execution surface via workspace.runtime.exec(source, { backend }). The premise: giving every agent its own container will not scale, so route the roughly 90% of agent work that is plain file manipulation into cheap isolates, and reserve containers for native binaries and package managers.The benchmark is more interesting than the pitch. On their own numbers, the FUSE mount is roughly 2× slower than ext4 and 3.6× slower than tmpfs on a full npm install, and up to 41× slower on bulk I/O. But it beats real disk on metadata-heavy work: removing 1,000 files at 0.66×, walking a tree at 0.72×, git init plus a 100-file commit faster than local.
Read that as a shape, not a score. Agent work is overwhelmingly metadata-heavy — listing, stating, moving, small edits — which is exactly where this design wins and exactly why the 10% claim is plausible.
llms.txt (1,699 bytes, the index) and llms-full.txt (26,839 bytes, about 4,054 words) on every deploy rather than maintained by hand — each page's HTML parsed, stripped of navigation, headers, footers and scripts, then converted to clean markdown. And a robots.txt carrying a Content-Signal directive, live and exact: Content-Signal: search=yes, ai-input=yes, ai-train=no. Read that as: index me, ground an answer in me, do not train on me.The deadline behind it. They cite Cloudflare's July announcement that from 15 September 2026 the defaults would block AI agents on pages displaying ads for new and existing free customers. Correction, 30 Sep 2026: the change Cloudflare actually shipped that day applies to new domains only, defaults ad-supported sites to a robots.txt “Disallow AI Training” signal rather than a block, and migrates existing Block settings to that signal. Sites that want agent traffic still do best to say so explicitly.
Honest note on the framing. They did not skip the Action tier to protect human visitors, which is how this gets retold. Their stated reason was simpler and more useful: the tier did not yet earn its complexity for a site of that size.
Why a scraper-builder should read an attacker's write-up. This is the exact inverse of the collection problem, and it is the same mechanism the guide keeps returning to: an agent cannot separate instruction from content, so any page or document it ingests is a potential instruction set. If you point an agentic browser at pages you do not control — which is the whole job — you are the target this payload was built for. The defensive takeaway from the authors is the honest one: understanding how the payload is assembled is the first step to defending against it, because there is no single string to filter — each of the five parts is innocuous alone. Control the actions the agent can take, not the text it reads.
Source: Zenity Labs.
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.
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.
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 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.
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.
<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.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.
This 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.
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 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
Oct 2026 update. Open Science Desktop (MIT, 1,788 stars on 12 Oct 2026), a research workbench, uses the same pattern: its agent drives the user’s own Chrome, logins intact, to harvest preprints, and it strips model-supplied
allowedDomains and session arguments so the app sets scope. Its “#1 on ResearchClawBench” is 22.8 averaged over 36 of 40 tasks (20.5 over all 40); the 17 Sep board ranks it seventh.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
_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-server.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).
When DIY cost
exceeds platform cost
If spending more than 2 engineer-days/month on anti-bot maintenance, a managed platform is cheaper.
When LLM-written code makes a scraper cheap to build, work out how much of the bill was writing code in the first place. Its share decides how far your break-even moves. One 2026 rerun of a 2023 build-or-buy model found break-even volume fell about two-thirds on light HTML sites but only about a tenth on sites that need a full browser render. The inputs show why. A hard-tier build is about $112.50 in tokens, but at 1.7 MB per rendered page and $8/GB, bandwidth is about $0.0136 per request. At 100,000 requests that is about $1,360, twelve times the token cost, and it comes back every month. Maintenance for simple fixes dropped from about 2 hours to about 30 minutes a month per scraper, but there was no gain when the anti-bot changed. So write the code yourself, and buy proxies, unblocking and browsers unless scraping is your business. If you reach for a visual workflow builder instead of writing the spider, check first what it leaves in your hands: exported code you could maintain without the tool, or a graph file that only that tool’s engine can run. Check the licence as well. Crawlflow, a node-graph editor on top of Scrapy, interprets its saved JSON at run time and exports no spider. At a 5 October 2026 check its public repo was two days old, with zero stars and no licence file, which by default grants no right to reuse the code. That makes it fine to try on a sandbox site, but not yet something to build a pipeline on. A builder that exports a plain spider keeps the write-it-yourself option open. One that only interprets its own format hands your maintenance to its maintainer, and you inherit that project’s pace and its survival. Treat these as one analyst’s model: the per-tier request counts are paywalled, and the per-request figures are derived from the published prices.
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.
robots.txt opens by saying automated access without express permission is strictly prohibited; the official APIs that are permitted tend to be expensive, rate-limited or closed to general use. That leaves three routes with different exposure: an official API where one fits, a provider or pre-collected dataset, or your own collector, which carries the most legal and maintenance weight. Do not count a scraping API as a separate legal route from collecting the data yourself; buying a finished dataset is the option that can genuinely move some exposure. In Reddit’s 2025 suit over scraped search results, the AI company that consumed a scraping API was named a co-defendant beside the API vendor. A cease-and-desist sent to a data vendor, by contrast, does not necessarily restrict how its customers use data already bought. A dataset purchase still leaves copyright claims over any protected material in it, staler data, the vendor’s margin, and the risk that the vendor gets blocked and the feed stops. Do not rely on any vendor’s representation that it is legally in the clear. This is one US law professor’s reading of unsettled law, not legal advice. If you build your own, stay logged out, since the guide’s cases put public, logged-out collection on the firmest ground, and expect no generic extractor: X serves feeds through internal GraphQL, LinkedIn through obfuscated internal JSON, and others through server-rendered HTML, each changing without notice. Judge the route before the code, not after.
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.
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.
Sep 2026 update. One more data point for convergence. Spidra (spidra.io, public beta; by its own description Playwright plus Gemini extraction) teased a “major announcement”, and on 29 Sep 2026 it shipped search with optional scraping of the top results in the same call, alongside crawl, batch, schema output and an MCP server. It publishes plan prices ($19–$249 a month; 2 credits per URL, 10 per CAPTCHA) but no success rate, benchmark or cost per accepted record. That the search launch was the teased announcement is our inference.
Oct 2026 update. More of the content those answers draw on: on 30 September 2026 Lara Translate, a translation company, published a “best no-code web scraping and monitoring tools” list. It has 13 numbered but unranked entries, prices taken from vendor pages on 4 September, and no hands-on test. By its own table, only Octoparse, ParseHub, Browse AI and HasData are no-code or visual. ScrapeGraphAI, which publicised its inclusion, is labelled a developer API. Read it as content marketing, not as an evaluation.
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. Treat that as the rule for the hardest vendors, not a law for every protected page, and test it on your own targets. In one open benchmark run in September 2026 (100 bot-protected URLs, four attempts, a language model judging success), browsers running straight from CI datacenter runners were far from zero: the best direct run passed 66 of 100, and a Firefox-based stealth browser passed 5 of 16 DataDome-stratum pages direct against 7 of 16 through a filtered residential pool. Several browsers scored higher direct than through either residential provider. So always run a no-proxy control arm beside each proxy arm, because a table that shows only the best proxy row credits the proxy for work the browser did. One run, small per-vendor strata, and both providers were sponsors of the benchmark site, so take the direction, not the decimals.
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. Before blaming a provider for a slow scraper, split one proxied request into phases and set it beside a direct one. The CONNECT wait, from the TCP connection to the gateway until the tunnel is ready, is the provider choosing and dialling an exit; time to first byte, counted from when the request is written, is that exit’s path to the target plus the target itself. Go’s httptrace exposes each phase, and small tools such as http-slow --proxy ... --compare-direct print them side by side. Repeat the measurement, because a rotating gateway hands each attempt a different exit and one sample says little. And read the status code first: a fast challenge page makes a target look quick when it has only refused you.Know where your IPs come from, and remember you are a guest
What a compromised residential pool actually looks like from the inside
content-security-policy and x-frame-options from responses so it could load targets in hidden iframes in the user’s own browser. That weakens the host’s security whatever opt-in screen came first. Scale claims deserve scrutiny too. A Northwestern IMC ’26 measurement found only 16 of 116 equipped extensions actively crawling during a session, so extension and install counts are an upper bound on real exit nodes. US nodes got up to 8.7× more tasks than other regions. If you defend a site, IP reputation and fingerprinting won’t help here, because these requests really do come from a residential browser. Watch for iframe-only sessions and the same structured extraction repeated across unrelated home IPs. And some of those addresses belong to people who never agreed to anything.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. If your measurement depends on what a user in one region actually sees, consider making the location real rather than buying the claim. A University of Potsdam audit of TikTok before three 2024 German state elections ran its 34 bots through servers physically located in each state, because the feed varies with where the user logs in. Its authors also note that residential and mobile IPs add cost and sourcing questions, and that proxied bandwidth becomes impractical for video-heavy platforms. A small machine in the target region, a rented server or even a single-board computer, can be cheaper and more trustworthy than a pool whose geography you cannot check. Keep the other vectors aligned to it. 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. If your measurement depends on what a user in one region actually sees, consider making the location real rather than buying the claim. A University of Potsdam audit of TikTok before three 2024 German state elections ran its 34 bots through servers physically located in each state, because the feed varies with where the user logs in. Its authors also note that residential and mobile IPs add cost and sourcing questions, and that proxied bandwidth becomes impractical for video-heavy platforms. A small machine in the target region, a rented server or even a single-board computer, can be cheaper and more trustworthy than a pool whose geography you cannot check. Keep the other vectors aligned to it. Before you tune the four vectors you control, measure the one you bought.
The scoring stack is five components: multiple reputation databases, an internal honeypot network, dynamic weighting of the reputation signals, automated checks at handoff, and continuous re-checking. Critically, the underlying reputation databases can be bought by competitors — the moat is the weighting, not the data.
What to do with this. Stop comparing pool sizes. There is no universally clean residential IP: quality is per-target and time-varying, and an address that works on a marketplace may already be dead on Google. The only honest benchmark is your own success rate on your own targets over a week, which is also the only number a vendor cannot sell you.
The HTTP/3 half. HTTP/3 runs over UDP and is now roughly a fifth of all requests, with HTTP/2 still near half and about 38% of sites advertising it via Alt-Svc. So a browser that cannot speak HTTP/3 to a site that offers it is now itself a signal. And the sting for request-based scrapers: curl_cffi hard-refuses HTTP/3 over both HTTP and SOCKS proxies — it returns “HTTP/3 is not supported over an HTTP proxy” and the SOCKS equivalent. That is a client-side refusal, not a server block.
The fixes. Cheap: launch with
--force-webrtc-ip-handling-policy=disable_non_proxied_udp (Firefox: media.peerconnection.ice.no_host and relatives). Proper: run the browser in a VM or container and proxy the whole environment at the network layer, not just web requests. “Switching to HTTP/3 doesn't remove protocol fingerprinting, it just moves it somewhere else.”Why this belongs in a scraping guide. When a provider tells you the pool is consent-based, ask which of the three checks they run: visible opt-in disclosure, a functioning opt-out, and a named sourcing partner. Then ask who verifies it — because at least one founder has said publicly that he relies on his suppliers' consent flows rather than his own screens.
One claim we could not stand up. A widely-shared teardown this week alleged a specific budget Android handset shipped a repurposed commercial proxy SDK plus a backdoor surviving factory reset. We searched for the writeup and could not find it indexed anywhere, so we are not repeating it as fact. The verified cases above make the same point without needing it.
Why you cannot just unplug it, and how they did anyway. A residential proxy network is decentralised by design — the exits are millions of consumer devices you cannot seize. So the takedown hit the three things those devices depend on to be a commercial product. The command-and-control layer: Google killed the accounts running the malware's C2 — which was itself hosted on Google's own services, a technique called living off trusted services so malicious traffic hides inside normal cloud traffic. The enrollment pipeline: Play Protect began disabling apps carrying NetNut SDKs and blocking new installs, so the pool shrinks as devices reset and drop off. The commercial front door: seizing the domains shut the storefront and stopped new customers. Google's estimate was that this cut available devices by millions.
The distinction every buyer should now be able to draw. NetNut sold four things, and they are not equal. Datacenter IPs are clean — clear ownership. Its ISP proxies came through DiviNetworks by an above-board arrangement: a revenue-share pitch to ISP partners, a GRE tunnel terminated on the partner's edge router, policy-based routing steering a slice of traffic out through NetNut — the partner signs and gets paid, and whatever you think of subscriber consent, the contractual chain is in daylight. The residential pool is the Popa part: sourced by distributing SDKs onto devices whose owners never knowingly agreed. Same vendor, same dashboard, two completely different provenance stories — and only one of them ends in a seizure banner.
The market's verdict, and yours. Alarum disputes the accusations and says it was never formally contacted — but the stock crashed more than 70% in a week, roughly 96% off its 2024 peak, and the company warned of a material adverse effect. For anyone running collection in production the lesson is concrete: provenance of your proxy pool is now a supply-chain risk you can be caught downstream of. “It was a big, professional vendor” is exactly what NetNut was. Ask where the residential IPs come from, prefer ISP or datacenter where the target allows it, and treat an unexplained too-cheap residential pool as the warning it is. This sits alongside the data-broker and legal material in Read this first.
Source: The Web Scraping Club, Pierluigi Vinciguerra.
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.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.
requests 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.Oct 2026 update. The same point now comes from a seller of proxies. Rayobyte’s founder and CEO says he often tells customers their proxies “might be fine”: sites weigh the browser, cookies and how you interact, down to mouse and typing behaviour, and his team runs hundreds of millions of scrapes a month on its own proxies. That is a vendor statement with no data behind it. His advice matches this guide’s: test a proxy swap, then keep investigating.
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.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.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
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.What practitioners are
actually shipping in 2026
Fresh insights from engineers actively solving these problems in production. Shared publicly on LinkedIn.
scrapy-phoenix repairs a spider mid-crawl; its own log records a repair silently delivering 445 of 566 items.The repo behind “about 40 seconds”.
lopuhin/scrapy-phoenix (Apache-2.0, created 3 Oct 2026, 2 stars, frozen after the talk): page objects refuse unfamiliar layouts, the crawl pauses, a Codex agent writes a new variant in a clean copy, a gate validates it and it is hot-loaded. The README reports 27 runs over nine cases ending as intended for 6–55 cents each, 566/566 items on a test store, and a refusal when prices went members-only. Its findings log is franker: in the live repo the agent grepped the site’s templates, a README naming the expected outcome and an /openapi.json of ground truth; and a variant for pages it was never shown took “Prev” for “Next”, shipping 445 of 566 items with no error until the gate was tightened.Beyond code. Giancarlo Morillo (LexisNexis) tracks cost per accepted record: a relevance-only crawl fetched 91% fewer pages and found 2% more entities but cost 3× as much, and a 100-page per-site cap found under half his baseline (81% at 500). Common Crawl as a reference exposed records his own code overwrote. Andrew Harris: a 3-day median age can hide a 14-month P99.
Honest caveats. Secondhand: no talk was watched here, quotes carry no timestamps, and every figure is the speaker’s own and unaudited. Doccraft states no tie to Zyte or speakers. The prototype ran on a local store and books.toscrape.com, not live targets.
requests with a sleep matches 200 async slots. Many independent targets: async wins on wall time. HTTP/2 helps only where a server multiplexes many small resources. Async has real costs: coroutines spread through the pipeline, tracebacks through an event loop are harder to read, and helpers assume requests. Its table: one site, requests; hundreds of targets, httpx async; mixed-maintainer team, requests; modernising, httpx in sync mode.Checking the arithmetic. 200 × 0.5 s = 100 s sequential is right. “Under a second” assumes all 200 in flight at once, but
httpx.AsyncClient defaults to Limits(max_connections=100, max_keepalive_connections=20), so 200 distinct hosts go in at least two waves, about 1 s plus DNS and TLS, unless you raise the limit. Polite sequential code is also slower than implied: sleep(P) after each response gives a period of P + L, so at P = 3 s and L = 0.5 s it runs about 14% under the allowed rate; a limiter that spaces request starts recovers it. And httpx does not “queue 199” by itself: with no limiter it sends them all, which is how a polite job becomes a ban.Two smaller fixes. HTTP/2 against an HTTP/1.1-only server costs no extra handshake: the protocol is chosen by ALPN inside the TLS handshake. And neither client addresses what this guide meets first on protected targets: both present a Python TLS fingerprint, so the switch that matters there is to
curl_cffi, not between these two. httpx’s latest release is still 0.28.1 from December 2024, worth knowing before a migration.Honest caveats. No benchmark, in the article or here; the pool and P + L points come from httpx’s source and simple arithmetic, not measurement. The decision table is a sound first cut, and the team-cost claims are opinion. A practitioner’s reshare repeated the argument nearly verbatim without linking the article.
gpt-4.1-mini) for a label and quote, keep the label only if the quote is a literal substring, and let any contradiction win. Its only numbers come from a 9-citation fixture dry run.ScraperHub/llm-citation-verification-service-with-crawling-api (MIT, created 5 Oct 2026, 0 stars). Fetch uses format=md with readability, refetches without it under 200 non-whitespace characters (both billed), and treats a site 404 or 410, a deep link redirected to /, and PDFs as unreachable or not found. Passages are 200-word chunks ranked by all-MiniLM-L6-v2 cosine similarity, top 3. The judge returns supported, contradicted or not_found as strict JSON; OpenAI by default, Anthropic or Ollama optional.The guard, read in the code.
quote_in_passages straightens curly quotes, maps non-breaking spaces, collapses whitespace and tests a plain substring of the joined top-3 passages; a miss downgrades the citation to not_found with confidence 0. Aggregation is fixed: any contradiction makes the claim contradicted, otherwise any support makes it supported, and judge confidence is ignored. That is the strong idea: the model interprets, code decides what counts, and an invented quote cannot survive.What it does not verify. The guard proves the quote is on the page, not that it supports the label: a real “100°C” sentence labelled as supporting “50°C” passes. Because one contradiction overrides everything, one judge error flips a claim. Soft-404 detection is four English phrases on pages under 800 characters; there is no bot-wall detector, so a 200 challenge page becomes
not_found, and a dead link answered with the homepage at the same URL, seen on 13 of 371 top sites in one Doccraft run, is not caught. Paywalled pages read as not found, multi-source claims stay not found, and the article says it checks support, not truth.Honest caveats. A vendor tutorial for its own API. The only results are a fixture dry run (9 citations, 2 supported, 3 unreachable; precision 0.22, reachable-only 0.33), so there is no accuracy figure for judge or retrieval, and top-3 is argued, not tested. Stored pages are kept 30 days for half a credit each: an audit trail worth copying with any fetcher.
The numbers. Usable records 6 (Jina) against 8 (Apify); explicit HTTP failures 3 against 1; extracted text 253,621 against 372,137 characters, 1.47×. Six sources were common to both and usable on both: Wikipedia, the IEA policy tracker, Brookings, an ECB blog, CNN and one trade site. Jina’s other four were Forbes (403), Reddit (403), Reuters (401) and a YouTube page of navigation; Apify’s were a CSIS analysis the authors call exceptionally useful, one more usable page, one 500, and one record the article counts neither usable nor failed. Jina did extract the IEA tracker’s country tables, which Apify missed.
What it actually shows. No URL was fetched by both tools and failed on only one, so the failure gap is a search-ranking difference, not an extraction head-to-head: Jina’s results included publishers that block automated reads, Apify’s did not. Costs are in incompatible units, 68,642 Jina tokens from a one-time 10M grant against $0.026 of Apify’s $5 monthly credit, and Jina does not bill failed requests. The 47% more text that the article scores as completeness is also more input tokens for the model reading it.
Honest caveats. One query, one topic, one run, by a party to the comparison. “Substantively usable” is never defined and the scorer is not named; the briefing verdicts (Apify slightly stronger on markets, Jina on societal examples) are the authors’ own. The article itself says blocking is volatile and results could change with other queries, limits or models. Copy the test design, not the ranking.
What he says is open. Whether terms can withdraw authorisation for trespass purposes, which a Van Buren footnote left unanswered; what a scraper is deemed to know from IP blocks, robots.txt or a cease-and-desist (he still expects a C&D recipient to be treated as told, citing Power Ventures, though Van Buren may have weakened that); what harm counts; who acts when an AI agent uses a customer’s account; and whether rotating IPs, patched fingerprints or CAPTCHA solving change the analysis, to which his answer is that nobody knows. X v. Bright Data, recorded in this guide as a dismissal, was revived on server-impairment allegations in November 2024 and settled in 2025: the flip-flop he cites as proof the doctrine is unpredictable.
Where the risk moved. On 31 July 2026 a New York judge let Reddit’s 1201 claims against SerpApi and Perplexity proceed, holding that a bot check can control access even while humans see the page. Goldman warns 1201 could swallow the other doctrines: it lacks their limits, scrapers can hardly design around it, and extracting only uncopyrightable prices does not help if the page carries copyrighted material. For vendors, Ticketmaster v. RMG (2007) found inducement partly in marketing that promised no blocks; courts look at who did what, not at indemnities, and routing through a scraping API does not move the risk.
What this is and is not. Forum answers, US law only, explicitly not advice, from a professor who calls the topic unteachable and sums it up as “ubiquitous but not clearly legal”. The Reddit order is a motion-to-dismiss ruling not reviewed on appeal, and he expects Google to replead its parallel claim. Use this as a map of open questions, not answers, and take a lawyer for anything commercial.
/doccraft-test-4f39159f8545 on each, from Mumbai on 9 October 2026 with httpx 0.28 and a declared research-bot user agent. Recomputed here from the published results.json (390 rows, 371 usable): 317 returned 404, 43 returned 200, and 11 something else (six 403s, two 406s, one each of 400, 202 and 429). The 43 split 14 homepage redirects (Steam, Dell), 6 “not found” text, 4 login redirects, 2 challenges (Walmart’s “Robot or human?”), 13 homepage-identical shells and 4 generic pages.The classifier, and the rerun. Rules run cheapest first: 404/410, 5xx, block (403, 429, challenge text, or a 202 under 50 words), login redirect, a deep URL landing on the homepage, “not found” text, then a fingerprint from a second random URL (same title, word count within 15%). Running the published
evaluate.py with calibrate.json reproduced every figure: of the 43, 23 not found, 4 login, 2 block, 13 unclear and 1 still a success; homepages 357 success, 13 unclear and 1 block, hCaptcha’s homepage, which contains the word CAPTCHA.The 13 the HTML cannot settle. Instagram, Twitch, Telegram (t.me and telegram.org), Dailymotion, Starlink, Tinder, JD, Ubisoft, DigiCert and others serve the same shell for every URL; scripts decide what to show. The rule therefore labels the homepage unclear as well: 14 good homepages lost their success label. The fix is to render, or test for the data you expected, such as a price or channel name. The production frame is secondhand from two Prague Crawl 2026 talks, not watched here: Logan Harless (UseString, “4 billion rows of data per week”) maps each terminal state to an action, and a 36-engineer team running 5,000+ retail spiders triages failures first.
Honest caveats. Homepages plus one made-up path per site, so real crawls meet states this never saw, like partial pages or changed layouts. One location, one client, a bot-labelled user agent; a browser or proxy may get other answers. English-only text rules. And the classifier was written and scored on the same 371 sites, so the 1-in-43 residual is in-sample; expect worse on new targets until the review loop the talks describe has run.
Where the bytes went. The default browser made about 917 requests per five-search session; 54% of its traffic went to third parties (maps, analytics, ads, chat). Blocking cut 4.0 MB to 1.1 MB (72%), but 76% of what remained was JavaScript the page needs and the scraper does not, since prices sit in the HTML. Chrome refetched Hyatt’s largest script (213 KB) on every search although it is marked cacheable. Request-based: 342 KB bootstrap per session (about 300 KB is the Kasada challenge script), 104 KB per search page, a 9 KB IP check. Five sessions of 100 searches each never re-challenged and averaged 106 KB.
The arithmetic holds. 4.0 MB over 178 KB is 22.5×; 1.1 over 4.0 is the 72% cut. Their model, (342 + 5 × 104 + 9) ÷ 5, gives the stated 174 KB per search with no restarts, and 244 KB if half of all attempts must re-bootstrap (two bootstraps per session on average). One figure is loose: they put the bootstrap at 41% of request traffic, while 342 of 5 × 178 KB is 38%, presumably retries. DevTools under-counted by 25% because it omits TLS handshakes, request headers and downloads cut off by the next navigation.
Honest caveats. One site, one anti-bot, one day, one proxy provider; no raw data or benchmark code is published, so only internal consistency could be checked. The two browser setups moved on at different points (load event versus HTML parsed), so the 72% is not pure asset blocking. Calls to the Hyper Solutions API went direct, not through the proxy, so the request-based figure excludes the paid solve. “Clean IPs matter more than pool size” is modelled, not observed: no session in the test lost its IP.
Why it matters to a buyer. The guide already warns that pools are resold under several brand names; this is the formal version. Source says its standards cover use-case vetting and compliance across the ecosystem, which fits a result the guide already carries: in a September 2026 benchmark published on ScrapingBee’s blog, Oxylabs and Decodo each refused 22 of 100 general-web requests, the same eleven domains both rounds. That reads as one policy rather than two, though neither vendor has said so. Buy two of these brands for redundancy and you may get correlated pools, refusals and outages.
The litigation that comes with it. Bright Data, then Luminati, sued the Oxylabs entities (Teso LT, Oxysales, Metacluster) over residential-proxy patents; a Texas jury found wilful infringement in November 2021, reported at $7.5M. The USPTO then invalidated claims in two of the patents, the Federal Circuit affirmed in August 2025 and the Supreme Court declined review in February 2026; Oxylabs says the verdict cannot stand, and its own patent suit against Bright Data is pending. Oxylabs is also a defendant, with SerpApi, AWMProxy and Perplexity, in Reddit’s October 2025 scraping suit in New York.
What this does not show. The announcement is the group’s own; it names no legal parent and no ownership chain for Decodo or Honeygain, and “independent companies” is its own description. Honeygain’s consent model is a vendor claim, and part of the patent history comes from Oxylabs’ legal timeline. Nothing here says the products are worse: treat it as a concentration fact to check before you count suppliers.
morluto/rea 6.4.0 is less one engine than about 180 MCP tools over other people’s: Ghidra, Hopper or IDA for native code, headless JADX, adb, Apktool and Frida for Android, Wakaru for minified bundles, a passive CDP page capture and HAR readers. Read from source and run once on a test HAR, it maps endpoints and code paths but keeps no cookies, auth headers or page text.jadx-headless-mcp 0.7.1 on JDK 17+, adb, Apktool, the optional Frida binding, Wakaru (Linux x64 only), mitmdump 12.2.3 (Linux only) and Binwalk or Unblob for firmware. JavaScript, Electron ASAR, source maps, .NET, Go and WASM are parsed in-process.What it gives a scraping team. For an APK: manifest, class search, decompiled methods and cross-references, then adb for logcat, files and app launch, and Frida for hooks you write. For a site: attach to a Chrome you started on a loopback port, list scripts, network metadata and WebSocket frame sizes, derive route and endpoint candidates from bundles, fetch source maps only when asked and without cookies, and list a page’s WebMCP tool declarations. It cannot read backend code, answering one commenter: it sees only what ships to the client.
What it deliberately throws away. The CDP capture never retains cookies or authorization headers, keeps DOM node names but not text, and reduces JSON bodies and WebSocket frames to value-free shapes. Run once here (npm 6.4.0, install scripts off) on a four-request synthetic HAR, the cookie and bearer values were absent from the output while a custom
x-signature value appeared four times: only known auth fields are redacted. The HAR endpoint-template view in its docs was rejected by the released CLI and is absent from the npm build; it lives on main. Every tool carries effect flags (network, foreign code, device changes), and SECURITY.md says plainly that REA is not a sandbox.Honest caveats. Source, docs and one offline run; no APK, browser or native target was tested and analysis quality was not measured. No analytics SDK appears among its dependencies, but your agent’s model provider sees every result. The post’s 67,000+ stars were 136,505 two days later; the repo’s milestone commits run from 20,000 on 8 Oct to 110,000 on 12 Oct, npm downloads rose in step to 59,665 on 11 Oct, and the stargazer list is not enumerable, so the curve can be described but not audited.
Oct 2026 update. On 15 Oct the repo showed 260,901 stars and 65,628 forks, so a later “250k+” claim is accurate, but the count nearly doubled from 136,505 in three days; its own commits mark 170,000 (13 Oct), 200,000 (14 Oct) and 250,000 (15 Oct), and 6.5.0 shipped on 14 Oct. Still unauditable. A practical tip from one user: keep the original HAR and the trace of where a signature is built beside the scraper, and diff a fresh capture when signing changes.
+= 8 makes a string, which shifts to 0 again. Reproduced in Node 26: bytes AA BB read as AA AA. Tidy the function to 0 up front and you read AA BB.valueOf (which returns the function itself) and then toString, which returns the function’s source text. StringToNumber of source text is NaN, and ToInt32(NaN) is +0, so f >> 3 is 0. For +, ToPrimitive yields the same string and a String operand means concatenation, so the register becomes the source text plus "8", then "88", still 0 under a shift. In Node v26.5.0 a function-valued register read [0xAA, 0xBB] as aa aa; a numeric 0 read aa bb. A function given its own valueOf returning 16 shifted to 2, so a rebuild must keep the coercion, not hard-code zero.Why the divergence hides. Per the account, a later write converts the original function to 0, adds the bits consumed and stores a number again, so both versions finish at the same position. A model or person “cleaning up JS quirks” makes a reasonable-looking change and decodes a different instruction with full confidence. It is the guide’s agentic-RE warning about runtime-bound semantics in concrete form, built from ordinary language rules rather than an anti-AI trick, and the counter is differential testing of every read against the original, not of end state.
Context and prior work. Mike Hearn, who wrote BotGuard, said on Hacker News (April 2025) that the VM exists mainly to make random encrypted programs cheap to generate, so adversaries cannot write a static analysis and must run them blind while detections feed delayed bans. The author says the environment contributes to keys that decrypt later stages, names variants for Gemini, accounts, YouTube, AdSense and Search, and says the reCAPTCHA variant, a near proof-of-work token, was removed this year.
dsekz/botguard-reverse (September 2025, 128 stars, reCAPTCHA variant) documents timing anti-debug and bit-addressed bytecode. For Google data, official APIs remain the cheaper path.The caveat. One practitioner’s account; no build, bytecode or code was published, and the trick was not observed here in a captured script, only its JavaScript semantics reproduced. “A few days with AI” is self-reported. The variant list and reCAPTCHA removal are unverified, as is a commenter’s view that BotGuard tokens are not cryptographically bound to the browser or hardware.
o200k_base tokenizer: 95 tools, 15,950 tokens, Playwright MCP’s 25 tools at 4,450. Then it loaded pages. One Playwright snapshot of Wikipedia’s “Web scraping” article took 31,565 tokens, twice the whole menu.Results outweigh the menu. Three pages, two tools (Playwright snapshot / Fetch whole page / Fetch default reply): Wikipedia 31,565 / 11,468 / 1,067; Hacker News 13,579 / 3,494 / 1,848; an Allbirds product page 10,824 / 1,217 / 1,217. Playwright MCP’s
browser_navigate now writes the snapshot to a .yml file and replies with a link: 82, 78 and 177 tokens. Fetch truncates at 5,000 characters unless asked for more. Apify’s mcpc 0.7.0 (Apache-2.0, about 1,000 stars) turns servers into shell commands with a cross-server grep: one search plus one tool’s full schema cost 454 tokens, against 10,977 for both browser servers’ 55 tools.Against this guide’s figures. The agentic section quotes roughly 13,700 tokens for Playwright MCP’s definitions and 17,000 for Chrome DevTools MCP; this count gives 4,450 and 6,527 for current releases. Versions, tokenizer and scope (bare schemas, no client wrapper) all differ, so read it as menus that shrank or were counted differently, not as an error in either. It backs the Code Mode card’s point from the other side: for a scraping agent the payload, not the menu, is the bill. Anthropic’s 55K-token five-server example and its 85% Tool Search saving, both cited, are calculated illustrations, not runs.
Limits. One run and one tokenizer; Claude and other models count differently. Only servers that need no key were measured, so Apify’s, GitHub’s and every commercial scraping server are absent. Three pages is not a distribution, and snapshot size tracks page length. The talk was Apify’s and the author sells research to web-data companies; the raw JSON and scripts are published, which is what lets the totals be checked.
Promise.all on an empty array through a custom constructor whose reject callback is a Proxy. Normally it resolves. With the Debugger domain on, V8 writes a private symbol onto that callback, the Proxy refuses it, and the page sees a TypeError. Reproduced here in Node 26.Runtime.enable alone stays clean, so it catches inspectors, coverage runs and agent tools, not a stock Playwright crawlsrc/builtins/promise-all.tq, the combinator runs if (IsDebugActive()) and then SetPropertyStrict(context, reject, kPromiseForwardingHandlerSymbol, True), a catch-prediction hint for the debugger. That key is a private symbol, and V8 refuses private-symbol writes to a Proxy before any trap runs. Promise.all catches the throw and passes the error to the page’s own reject callback, which is the Proxy. The constructor is page code, so the page sees both. A second debug-only write (kPromiseHandledBySymbol on each then result) sits a few lines later.Reproduced here, in-process. Node v26.5.0 (V8 14.6.202.34) driving its own inspector session: a plain run returned
resolved; after Runtime.enable alone, still resolved; after Debugger.enable, TypeError: Cannot pass private property name to proxy trap; after Debugger.disable, resolved again. The signal follows the Debugger domain and is reversible. It was not run in a Chrome build; V8 is shared, so it should carry over, but that is untested.Who actually turns Debugger on. Playwright’s
crPage.ts and Puppeteer’s Page.ts never send Debugger.enable; both send it only when JS coverage starts (crCoverage.ts, Coverage.ts). The open-source REA reverse-engineering server sends it during page capture. So the probe mostly separates instrumented sessions, and probably developers with DevTools open (not tested), from ordinary automation. For a defender that means a noisy, low-prevalence signal; for a crawler owner, a reason to keep coverage and inspection tools off production targets.The honest caveat. One researcher’s post with a code image and no repo; he says he has moved to the evasion side, and his company sells web-access infrastructure. The wider claim, that many outcomes besides console output change under CDP, is plausible but only this vector is shown. The single comment, from a proxy vendor’s CEO, adds only that patching CDP leaks never ends. One V8 change would close it, as the stack-getter tell closed in Chrome 138.
self.__next_f.push on 33, __NEXT_DATA__ on 29, Nuxt on 13 and Redux-style state on 9. On DeepL’s pricing page the App Router payload held 23 price entries in four currencies while the page showed four, in euros. Rechecked here: 23 again.__NEXT_DATA__ tag. Join the self.__next_f chunks and regex known field names, because the format is internal to React and a parser library failed on both pages tried.httpx 0.28 from Mumbai) reused its Tranco top-1,000 run. Of 390 homepages with readable content, 215 carried JSON-LD, 33 the App Router stream, 29 the Pages Router __NEXT_DATA__ tag, 13 Nuxt and 9 window.__PRELOADED_STATE__ or __INITIAL_STATE__; 62 (16%) were Next.js in all. The method is the one this guide already teaches: pick a visible value, search view-source for it, and go to the Network tab when it is absent. Nuxt 3 moved page data out of window.__NUXT__ into a __NUXT_DATA__ tag holding a flat array whose numbers index other entries, so objects are rebuilt by following indexes.The DeepL recheck. The script joins the
self.__next_f.push([1,"..."]) chunks from deepl.com/en/pro, decodes each as a JSON string and regexes packageId and basePrice pairs: 33 chunks, 23 price entries in CAD, EUR, JPY and USD. Rerun here on 12 October from another network: 34 chunks, 246,266 characters, 23 entries in the same four currencies. The njsparser library failed on both App Router pages the author tried, and React treats the format as internal, so expect breakage.The
_next/data shortcut. Pages Router sites may serve /_next/data/<buildId>/<page>.json, the build ID read from __NEXT_DATA__. TED’s homepage data came back at 288,751 bytes against 589,257 for the HTML; rechecked here, 280,807 against 584,748, status 200. It is not general: of the 29 Pages Router homepages, 26 still had the tag and 9 answered the route, and the ID changes per deploy, so re-read it on a 404. Genius keeps state in JSON.parse('...'), escaped twice and normalised by ID; chompjs handles object literals that are not valid JSON.Caveats. Homepages only, one run, one location, from a firm that writes for web-data companies. Prices and stock often arrive later from an API, so absence in the source proves nothing. Paths such as
sailthruDataContent are internal names that can change in any release. The extra data is the hazard as well as the prize: payloads can carry internal IDs and personal data the page never shows, so take the public fields you need and drop the rest.ETag and Last-Modified you stored and an unchanged page can answer 304 with no body. A test of 368 top homepages and 224 inner pages found 41% and 40% returned a 304, each skipping a median ~45 KB, while 49% sent no validator at all. Google says only 0.017% of its own fetches are cacheable.httpx 0.28, Mumbai) took the homepages that returned content in its Tranco top-1,000 run plus the first URL in each sitemap, sent two plain requests a second apart, then one with If-None-Match and one with If-Modified-Since. Homepages / inner pages with a 200: 368 / 224. ETag 125 (34%) / 74; Last-Modified 123 / 71; neither 181 (49%) / 110 (49%); a 304 to either conditional request 151 (41%) / 90 (40%). Median full responses on the 304 pages were 44,582 and 45,219 bytes, and every 304 body was empty. The author’s analysis script, run here on the published JSON, reproduces each figure.Where validators lie. 17 homepages and 6 inner pages changed their ETag between two requests a second apart, so it can never match. 38 and 21 sent an ETag but answered
If-None-Match with a full 200 (23 and 18 once the rotating ones are removed), and 15 and 5 of those gave a 304 on If-Modified-Since. 13 and 5 stamped Last-Modified with the request time. And 3 homepages and 2 inner pages returned a 304 although their visible text had changed between plain requests, so a crawler that trusts every 304 keeps a stale copy. Scrapy has this built in: HTTPCACHE_ENABLED with RFC2616Policy revalidates and counts httpcache/revalidate on a 304.Google’s number is not a contradiction. Google’s crawler docs say it supports both validators and uses the ETag when both are present; a 9 December 2024 Search Central post says cacheable fetches fell from about 0.026% a decade earlier to 0.017%. That counts all of Google’s fetches over real recrawl gaps, when pages have changed, while this test re-asked within seconds. Together they bound the method: a large saving where sites support it, nothing where they do not. Where nothing is sent, hash the main text: 28 homepages changed visible text between the two plain requests, but 150 changed their raw HTML.
Caveats. Seconds-apart requests overstate the 304 rate you will see at daily or weekly intervals, and a 200 after a real change is correct behaviour. One inner page per site, the first in its sitemap, from one location and one client; CDNs and anti-bot layers may answer browsers or proxies differently. The author reports three full runs differing by one point at most. The saving that matters is bandwidth on per-GB proxies, plus the parsing and embedding a 304 lets you skip.
The retest, recomputed. Doccraft (Satyam Tripathi, 8 October 2026) took the first 2,000 HTML 200s from one CC-MAIN-2026-39 WARC; 1,972 had WET text, and 664 were tagged English first. Each got WET, trafilatura 2.3.1 with FineWeb’s
favor_precision settings and Resiliparse 1.0.9 main_content, all run through datatrove’s FineWebQualityFilter. Passes: 68 (10%), 336 (51%), 224 (34%). Words on passing pages: 85,002, 170,495, 170,387. Extraction: 30.0 s against 1.8 s on an M3. The published JSON reproduces all of it. WET failed mostly on line_punct_ratio (431 pages), which is what menus look like.Where each breaks. trafilatura returned nothing on 86 English pages, mostly grids, logins and spam, but about 8 were real posts, 6 on Blogger; default settings recovered 6 of the 8. It sometimes kept a newsletter sidebar. Resiliparse keeps titles, dates and bylines and often repeats the title, which can trip FineWeb’s 1% repeated-line rule on its own. One scrambled-sentence spam page passed in its Resiliparse version: a heuristic filter is not a spam detector. This sits beside the ReScraper card, where four heuristic extractors landed within 0.003 Core of each other and a model reading the whole page beat all four.
Caveats. One WARC file, its first 2,000 records rather than a random sample, English only by Common Crawl’s tags. The filter was designed on trafilatura output, so trafilatura’s higher pass rate may partly be home advantage, and passing a filter measures yield, not model quality; DCLM’s training runs are the stronger evidence. The author writes for web-data companies; scripts and per-page data are published.
quotes.toscrape.com/js/, while saying plainly the gain is memory, not speed. Doccraft (Satyam Tripathi, 6 October 2026, Apple M3, five runs each) got medians of 2.92 s and 252 MB for jsdom 29, 2.88 s and 468 MB for headless Chromium, and 2.66 s and 35 MB for Lightpanda 1.0.0, released 2 October under AGPL-3.0. All three found all 10 quotes.The silent difference. jsdom’s order was exactly the reverse of Chromium’s. The page writes its quotes with
document.write(), which jsdom handles differently from a real browser: complete data, no error, wrong order. Lightpanda matched the browser. Wherever position is data, such as rankings or search results, that is a corrupted field that no memory benchmark shows and no exception flags.Real sites. The 29 homepages that needed JavaScript in the 1,000-site test (this guide’s 5% card) were each loaded once from Mumbai on 3 October; Chromium got content on 26, and success meant at least half its word count. jsdom 29.1: 3 (example.com, onet.pl, playrix.com). happy-dom 20.14: 1 (Springer). Lightpanda: 12, including Spotify, Roblox, Steam Community, Intel and Font Awesome, at a median 96 MB peak across all 29. Recounted from the published JSON, every figure matches; 12 of the 26 worked only in the full browser. Disqus gave Lightpanda 555 words and Chromium none, a reminder that the baseline is itself one load.
Caveats. One run per site and one location. Chromium waited 5 s after load with a bot user agent; Lightpanda ran
fetch --dump html with its defaults and no extra wait, so some misses may be timing or blocking rather than engine gaps. Half of Chromium’s word count is a coarse bar. The post says 26 sites in one place and 29 in another: 29 tried, 26 scoreable. Hydration saves memory only on pages you can reach; the talk itself noted a non-browser TLS handshake can be blocked before any script runs.robots.txt). Italy came out largely open; Argos, Currys, Fnac, MediaMarkt and idealo mostly refused. Per-site counts were not published.robots.txt, fetch the HTML, check whether product prices sit in the source. Three verdicts follow. wget: server-rendered, plain HTTP is enough. browser: content hydrated by JavaScript or behind a challenge, an order of magnitude dearer per page. blocked: a 403 even on robots.txt, or no answer, which he reads as an opt-out, not a puzzle. Named open sites include Unieuro, Euronics, IBS, laFeltrinelli, Tannico, Farmaè and Kasanova in Italy and Alphamega in Cyprus; in the north only Thomann, Otto and Rueducommerce. He frames robots.txt pointing to a sitemap with last-modified dates as a cleaner and more honest seed than crawling navigation.What a re-check found. No per-site table or code is released, so the 18 named sites were re-fetched here once with
curl and a Chrome user agent on 12 October 2026, from Pakistan. All seven Italian shops, Thomann, Otto and Rueducommerce returned 200 on robots.txt and homepage with currency markers in the raw HTML. Argos, Fnac, Darty and idealo (Akamai) and Currys and MediaMarkt (Cloudflare) returned 403 on robots.txt itself; ASOS did not answer within 15 seconds. That is 17 of 18 labels held. Alphamega did not: Cloudflare sent a 403 to robots.txt and homepage alike.Where it sits in this guide. The 544-homepage test found only 5% needed JavaScript and 14% blocked a plain request but admitted a browser, so “blocked to
curl” and “blocked” are different verdicts; Bamberg showed how much of a browser block is one header. The only comment adds two refinements: many Cloudflare sites clear after one real browser visit whose session can then be reused over plain HTTP, and product pages are often server-rendered while category and search pages hydrate, so classify per page type, not per domain.Honest limits. One practitioner’s snapshot with no date, location, tool versions or per-site results, and the 71 are not listed, so the north–south split is an impression, not a measured rate. Our check is one run from one non-European vantage with
curl’s own TLS under a Chrome string, the mismatch this guide warns about; the Alphamega 403 may be geography. A 403 on robots.txt is a firewall rule, not a stated policy. He invites corrections and plans a per-country map.Why the grid is the transferable part. The endpoint step is the guide’s API-first recipe, which already carries a Maps report. The tiling is new here and applies to any search that truncates per query: store finders, job boards, property maps. Google’s own Places API Text Search (New) returns at most 60 results across all pages, 20 per page, and accepts a rectangular
locationRestriction, so tiling is also how you cover a city through the sanctioned API. This card’s extension, not his: treat a cell that returns the full cap as truncated and split it again, size cells by density rather than area, and dedupe on the place ID, since a listing near a border appears in several cells.The arithmetic. At 100,000 listings and 3–5 seconds each, one browser needs 83 to 139 hours for details alone, so the case for skipping the browser holds even before search. Parallel workers on an internal endpoint also multiply request rate from your IPs, and the post names neither the rate, the proxy setup nor any blocks.
Honest limits. One practitioner, one job, no code, no endpoint named, no cell count, duplicate rate, field list or completeness check against a ground truth, and no comments to test it against. Scraping Google Maps is against Google’s terms, and the Places API terms limit caching and storage. Business names and phone numbers can be personal data under GDPR, so check the basis before you build a lead list.
Where the money goes, from the thread. The post does not itemise the bill. The comments point at bandwidth: one reader said it “could be done much cheaper” on proxies without bandwidth metering, and the founder replied that ScrapeBadger had been the cheapest on the market and unprofitable on Google even before the changes, is still below the market average, and asked for proxies that “hold up on Google without bandwidth metering.” That fits what this guide has measured elsewhere: a working Google request now needs a real browser, a warmed residential exit and extra
/goto requests, all billed per GB. A product lead added the defender’s view: raising the cost of success is the point of a deterrent. A question on the price of 1M ten-page SERPs went unanswered.The price, checked 12 October. ScrapeBadger’s Google SERP page lists Web Search at 7 credits per request, plus one credit when a deferred AI Overview is fetched; pay-as-you-go credits are $0.15 per 1,000, so $1.05 per 1,000 searches before subscription discounts of up to 64%. Third-party comparisons still quote 2 credits, or 3 plus 0.1 per result; their dates are unclear, so the before-and-after ratio is not confirmed here. Failed requests are not charged.
Honest limits. This is one vendor describing its own costs, with no absolute figures, no proxy type or per-GB rate, and no definition of “success”. The 3–4× is infrastructure, the customer’s 3× is price, and neither can be checked. A side comment that Reddit is shutting RSS feeds and its public API was not verified. Read it as one data point on direction: stable Google access got more expensive this autumn, and a cheap SERP API is a price that can move.
Oct 2026 update. ProxyScrape’s founder, a proxy seller, offers a hypothesis: Google fears blocking real users, or showing them fake data, more than it fears scrapers, so a client that does everything right is not blocked, it just pays more. A web-collections manager at Recorded Future added that Google’s SEO bargain with sites makes confident blocking hard, so it slows traffic at scale and fingerprints search terms; another reader warned that some sites now serve spoofed data to valid-looking requests. Unmeasured, but it fits the pattern above.
The playground screenshot. A live “Product & sellers” call for one catalog ID returned HTTP 200 in 8.22 seconds and 11.8 KB, with a
google.com/search?ibp=oshop&q=*&prds=catalogid:… URL behind it. The UI notes that Google lists sellers about five at a time, like its “More stores” button, and exposes seller_pages from 0 to 20. The sample product is a LEGO set at a typical price of $300.The question worth asking, from the thread. One commenter said the number to publish next is the shape of the missing 5.5%: if failures cluster by query type, retries are cheap; if they cluster in time, something upstream is throttling, and a 2.5-hour average hides both. The author agreed to look. Pair it with the guide’s method for reading success rates: who defined “returned results”, who chose the queries, at what rate, and does it hold per query class. Per-result billing also depends on the vendor telling a genuine empty SERP from a refusal, which this guide’s warm-up cards show is easy to get wrong.
Honest limits. Vendor-run, one day, self-reported, with no harness or data. ProxyScrape’s own site did not resolve from where this was checked, so the product page was not read. Its Apify listing, a separate channel, prices organic Google SERPs at $0.40 per 1,000 requests, charges only successful responses, and shows 13 total users. 99.96% for Shopping has no sample size, and 8.22 seconds is one call, not a latency figure.
v20261001 split into 44 shards, ten-day crawl windows plus Wikipedia and StackOverflow. Diffbot’s docs confirm self-hosting through sales, an 8 TB target index and English only.v20261001-searchlite: 44 shards, a 91 GB frequently-changing set (company reports, site crawls), a 189 GB stable set (Wikipedia, StackOverflow), and temporal shards of roughly ten days each from October 2024. He picked 22: the two base sets, every window from 13 February to 24 September 2026, mostly 75–132 GB each, and a 132 GB oldest-and-newest set, about 2.6 TB with 293 GB still downloading.What Diffbot documents. Its Web Search page calls the product “web search in a box”: index, retrieval and reranking on one machine or cluster, about 300 ms latency target, full page chunks stored so no fetch is needed, runs offline with zero telemetry. The target production index is 8 TB, said to cover “99% of daily driving search requirements”, on about 10 TB of NVMe, 64 GB RAM (index size over ten), a 5 GB GPU and 32 cores. The index is English only, and Facebook, Reddit, LinkedIn and Instagram are “generally off limits”. Self-hosting is “reach out to us”; no public price or download. The hosted API gives 100,000 free queries a month at 60 per minute, paid down to $0.02 per 1,000.
Why it matters for scraping. Most agent search today is a SERP scrape behind an API, and this autumn’s cards show that path getting dearer: warm-up,
/goto, vendor price rises. A local index needs no Google request, proxy or per-search fee; the cost moves to disk and to staying current. A slice ending at 24 September cannot answer about 6 October, and an index that skips closed platforms answers nothing about them.Honest limits. A vendor employee demonstrating his employer’s product, with no retrieval-quality figures for the slice against the full index or Google. Whether individuals can download shards for free is not documented; neither are the “searchlite” build or the model he names.
morluto/rea (MIT, created April 2026, 132,546 stars) registers an MCP server that hands a coding agent Ghidra, Hopper or IDA, JADX, HAR and mitmproxy readers, and a CDP page capture. One developer used it to rebuild Apple’s US homepage and a chatbot’s interface. His own verdict: it sees what reaches the browser, never backend logic, prompts or data.npx rea-agents setup registers it with Claude Code, Codex, Cursor, Gemini CLI and others. Native binaries need Hopper, Ghidra or IDA; JavaScript and Electron analysis recovers modules, source maps and routes; Android goes through JADX and adb; .NET and firmware are covered too.The web side. It attaches to a Chrome you start on a loopback debugging port and, per its docs, never navigates, clicks or evaluates page code. It captures DOM, scripts, resources, network and console metadata and WebSocket frame sizes; a bundle analyser derives route and endpoint candidates; a HAR view groups requests by method, origin and path template, folding numeric IDs, UUIDs, long hex and ISO dates into placeholders, with no network activity. Passive is not invisible:
CdpPageCapture.ts sends Debugger.enable, which a page can detect through Promise.all, and the docs say coverage mode changes timing and JIT behaviour.The test and the verdict. The Apple rebuild is labelled on the page as an unofficial reconstruction captured on 11 Oct, not affiliated with Apple, with local sample search results; the chatbot prototype’s answers are hardcoded. Commenters agreed it copies what you see, not what makes it work, and one asked how it handles authenticated flows and dynamic data, which went unanswered. Another argued any coding agent could do this from a prompt.
The honest caveat. One tester, no measure of fidelity, and no scraping use tested. A star count that large in six months measures attention, not quality. The README limits use to lawful research, interoperability and software you own or are authorised to examine; rebuilding a commercial site’s front end and brand raises copyright and trademark questions the tool does not settle. This guide did not run it.
Oct 2026 update. By the evening of 12 Oct the repo showed 136,505 stars and 30,661 forks; an earlier post on 10 Oct had said 67,000+. Its own milestone commits put 20,000 on 8 Oct and 110,000 on 12 Oct, npm downloads reached 59,665 on 11 Oct, and the stargazer list is not enumerable, so the surge is unaudited. One correction from an offline run of npm 6.4.0: the HAR endpoint-template view is on
main only, not in the released package.Why a scraper should care. Nothing here needs JavaScript or a vendor. Request rate, header anomalies and page-to-page navigation shape are readable from an ordinary access log; JA4 needs a TLS terminator that computes and logs it, which many CDNs and edge proxies now offer. A client that copies a browser header set over a library handshake trips two of the four at once, header anomaly and JA4 mismatch, and adding speed makes three. Fusion rewards exactly the multi-signal outliers, so the cheapest move for a crawler is to stay unremarkable on each axis rather than perfect on one.
Where it sits in this guide. It is the small-site, in-house version of the four-layer trust score that opens the detection section, and of the vendor cross-checks described there. The design choice worth noting is the review line: a score that queues a human look rather than a block keeps false positives cheap, the same trade-off a commenter on another post raised about blocking real customers. It also means a flagged crawler may never see a block at all: a review queue can end in a quiet rate limit, a ticket to an abuse desk, or nothing, and a scraper owner reading only status codes cannot tell which. Watch for slowing responses and thinning content, not only for 403s.
The honest caveat. Synthetic data with planted outliers, so the clean gap between 0.39 and 0.83 is what the generator made, not what real traffic looks like. No code, signal definitions, weights or threshold rationale were published, no precision or recall, and comments were restricted with none visible. It illustrates a pattern; it measures nothing. One analyst’s demo.
tavily-headless (created 11 Oct 2026, 1 star, no licence) is a small Python client over Tavily’s paid API, not a reimplementation: it still needs a Tavily key. Its one material change is include_raw_content=True on search. The advertised include_answer off is already Tavily’s documented default, and Research reports are still written by Tavily’s model.client.py, an argparse CLI, a standard-library ThreadingHTTPServer and one HTML page with a vendored copy of marked (MIT) for rendering reports. Search defaults to basic depth and 10 results with include_raw_content=True and include_answer=False. Extract takes up to 20 URLs (Tavily’s documented ceiling) and returns Markdown. Research creates a task, polls every 5 seconds for up to 600, then returns content, sources and usage untouched. The web UI binds to 127.0.0.1:8000, keeps the key server-side in .env, and has a Raw JSON toggle.Checked against Tavily’s documentation. On
/search, include_answer and include_raw_content both default to false, so forcing the answer off changes nothing; turning raw content on is what changes the payload. The docs also say search_depth controls “how results[].content is generated”, so the snippet field is itself shaped server-side (basic, fast and ultra-fast cost 1 credit, advanced 2). On /extract, passing a query reranks chunks of about 800 characters; the wrapper does not pass one, so you get whole pages, billed at 1 credit per 5 successful URLs on basic depth.Why it matters in a pipeline. When a search API is your acquisition layer for an agent or a RAG corpus, store the page text with its URL and run your own model over it, so what you audit is what the agent read. That is the guide’s retrieval-layer argument from the agent side: a search-backed corpus already has the shape of someone else’s index, and letting the vendor summarise it adds a second layer you cannot inspect. It also keeps the provider swappable, the point of the agent web-access funding card.
The caveat. A personal project announced the day after it was created. The GitHub API on 12 Oct 2026 showed creation and last push on 11 Oct, 1 star, 0 forks and no licence file, which under default copyright grants no reuse permission. There are no tests. “Headless” means no local model, not a headless browser: nothing is fetched locally, Tavily does the crawling. Research remains an LLM-written report from Tavily, the opposite of “zero AI”. Not run here, since it needs a paid key.
httptrace: DNS, TCP, the proxy CONNECT wait, TLS, time to first byte and download, then prints rule-based findings. --compare-direct runs the same URL with and without your proxy, and --repeat reports p50, p95 and p99.DNSStart to DNSDone; TCP is the connect to the origin, or to the proxy when one is set; the proxy phase is the gap from that connect finishing to the TLS handshake starting (or to the connection being handed over, for plain HTTP), so it is inferred, not read from the CONNECT reply. TLS is the handshake. Time to first byte starts when the request is written, so it excludes connection setup; download runs to the end of the body; total includes redirects. Phases that do not apply print a dash. Findings are fixed rules worded as “may indicate”: DNS over about 100 ms, TTFB dominating the total, a large compressible body sent without gzip, br or zstd, high variance across repeats, and no connection reuse.Modes that matter for scraping.
--proxy takes HTTP or HTTPS proxies only (no SOCKS), redacts passwords in output and otherwise honours HTTP_PROXY. Besides direct against proxy there are cold against warm connection, HTTP/1.1 against HTTP/2, IPv4 against IPv6 and two endpoints; one at a time. --resolve host:ip pins an origin while keeping SNI and Host, so you can time past a CDN edge. JSON output and exit codes (2 for latency over a limit, 3 for status, 4 for TLS) make it a CI gate.Where it helps. The one reply on the post put it plainly: a single total makes it easy to blame the server, though the delay can sit before the request reaches it. Through a rotating residential gateway, the
CONNECT wait covers the provider choosing and dialling an exit, and TTFB covers that exit’s path to the target. Each run may land on a different exit, so read p95 over --repeat, not one sample.The caveat. A personal tool: the GitHub API on 12 Oct 2026 showed creation and last push on 6 Oct, 0 stars, 0 forks, MIT, with CI and unit tests. Not run here. Go’s client has its own TLS fingerprint and no browser headers, so a protected target may answer with a fast challenge page, and then TTFB times the refusal; check the status first. Through a proxy the target’s DNS happens at the proxy, so the DNS row describes the proxy host. In the code read, each compare leg is a single request.
robots.txt disallows /robots-disallowed and sets Crawl-delay: 2 for everyone.What it offers. Extraction cases: static paging, endless scroll, load-more buttons, review JSON hidden in the HTML, an
X-Secret-Token API, GraphQL background requests, CSRF-locked paging, cookie and iframe logins, localStorage, a cookie popup, mismatched charsets, text hidden with invisible Unicode, and file downloads. Blocking cases: a 200 page that says you are blocked, a redirect on a bad Referer, a cookie that keeps you blocked, a simple antibot gate. Crawler cases: robots and meta-robots traps, X-Robots-Tag, canonicals and duplicates, a page minting 10 new session-ID links per visit, the calendar, a 10-hop redirect chain plus a loop, links only in JSON-LD, data-href or a Link header, a base tag, a 50 MB body, a 60-second drip, a wrong Content-Length, a 50-by-100 sitemap index, a gzipped sitemap and basic auth.Spot-checked here (12 Oct 2026, one client, a few requests).
/blocked returned 200 with a “You’ve been blocked” page; /antibot/easy 403; /huge-page streamed 53,248,026 bytes in 6.9 seconds; /slow-drip gave 17 bytes in 6 seconds before the client cut it; /wrong-content-length made curl abort with no body. But /rate-limited, documented as a 429 after 3 requests in 10 seconds, returned 200 six times in a row, so confirm a fixture fires before trusting a green run. One commenter described a real-world cousin of the huge body: a 302 to a 10 GB speed-test file, to drain someone’s residential bandwidth.The caveat. A vendor-built sandbox: the cases are synthetic, the antibot is labelled simple and is not Cloudflare or DataDome, so passing proves crawler hygiene, not stealth. Some cases (a classify API, WebMCP via a cloud browser) demo Scrapfly products. Most scenarios were read, not exercised.
The platform underneath. A browser fleet for JavaScript, anti-bot, consent and pagination; Spark, Scrapy and Temporal for processing; agents on Ray; a REST API, CLI and an MCP server that forwards the caller’s own credential. It says it is built on Anthropic’s Claude Agent SDK. Plans: Starter €10 a month (SaaS only, up to 3 machines, Scrapy), Pro €299 (up to 25 short-lived VMs, Spark), managed Kubernetes €200 per node, BYOC Soft €99 per node and BYOC Hard €599 per node with a three-node minimum. Short-lived browsers and Spark executors are not licensed: 10 per Soft node, 25 per Hard. Indicative SaaS rates include residential proxy (DataImpulse) at about €1.20 per GB and a Hetzner VM at €0.05 to €0.09 an hour.
Why it matters for cost. The guide compares per-gigabyte and per-success billing; this is a third meter, where the vendor’s share ignores how many records you pull and your own compute and proxy bill carries the volume. The homepage line that “your bill does not grow with your results” holds only for BYOC: the hosted tier bills compute, storage, proxy and model tokens as prepaid credits at cost plus 30%.
The caveat. All of this is the vendor describing itself. Access is by invitation; there are no published success rates, customers or benchmarks; the anti-bot claim is untested; prices are marked indicative and exclude VAT. Its comparison table, which ticks every column for itself against Apify, Bright Data, Firecrawl and Databricks, is self-assessed from “public product information, September 2026”. Nothing was run here.
The talks worth looking for. Shane Evans (Zyte) on what AI actually changes in scraping; Akshay Philar (Zyte) on the antiban arms race and the open web; Jason Grad (Massive) on residential proxy sourcing, billed as where proxy IPs come from deciding what you can trust; Andrew Harris (AB Harris Methods) on AI companies purchasing web data; Joe Dye (Byteful) on TCP/IP fingerprinting; Pravin Khandke (Insight Global) on hybrid agents for finance extraction; João Drummond (Crawly) on agent coordination; Konstantin Lopukhin (Zyte) on a spider that rewrites itself; Logan Harless (UseString) on a multi-armed bandit approach; and a legal panel with Scott Lashway and Sally Anne Hinfey.
Where it lands in this guide. Ban management and TCP/IP fingerprinting sit in #detect and #antibots; proxy sourcing in #proxies, next to the botnet and SDK-sourcing cards; self-healing spiders and bandit selection in #ai and #proxies, where the guide already has a Thompson-sampling proxy card and an autonomous-fixer result that lost to engineers. The buyer side of web data, what AI firms pay for and on what terms, has no card yet.
Honest limits. This is a programme, not content: no talk was watched, and the Austin event has already run. Zyte organises the summit and sells extraction, browsers and proxies, and several speakers are vendors pitching their own approach. The organiser says talks are published on the Extract Summit YouTube channel; whether the workshops are recorded is not stated. The European edition in Dublin (10–11 November, Radisson Blu) may differ.
Oct 2026 update. Ten Austin recordings are now on the Extract Summit YouTube channel (titles checked 15 Oct), and a Doccraft roundup summarises them; see the card on what the talks said. Andrew Harris’s buyer-side talk, the gap flagged above, says buyers now ask what was missed and how the vendor knows, and that vendors fail most on provenance and reproducibility. The talks were still not watched here.
Math.random() <= .05, the site’s own DataDome settings. All four curl requests got a 403; the User-Agent only switched the challenge type.dd.leboncoin.fr/tags.js, 123,989 bytes of minified JavaScript with readable method names and a plain base64 string table, served from S3 behind CloudFront on a hostname that looks like the site’s own. Around it sit an edge component that answers a navigation with the page or a 403, a collector at /js/, ct.captcha-delivery.com for the device-check and CAPTCHA bootstraps, and geo.captcha-delivery.com for the challenge page. Every claim is graded: string in the table, code that can run, or seen on the wire.What went over the wire. One form-encoded POST from the main frame, 5,069 and 4,996 bytes in two captures, nine fields in the same order:
jspl (the opaque signal payload, 4,698 to 4,771 characters), eventCounters (an empty [] in a run with no interaction), jsType (ch, the challenge collection), cid (128 characters, the cookie’s length), ddk (30 hex characters, identical every time, so a site key rather than a session value), Referer, request, responsePage and ddv. In Manus’s eleven samples the home page loaded, but a same-session HEAD and fetch both drew 403s, and every curl request got a 771-to-774-byte challenge bootstrap with or without a Chrome User-Agent (only rt=c versus rt=i changed).Checked here, and how it relates. The tag fetched on 12 Oct 2026 is already 5.10.0 (132,516 bytes, modified 16 Sep), still on S3 via CloudFront, and still runs
if(Math.random()<=.05) to add window.ddoptions as opts and window.ddCaptchaOptions as xhr_opts. The guide’s DataDome profile and Grainger case are about which paths a per-site model guards; this is the client half. Unlike the Hermès listing endpoint, where a same-site fetch got a 200 with no token, the same-session fetch here was refused. And unlike Kasada’s virtual machine, this sensor is legible.The caveat. One site, one tag build, a handful of captures. The fingerprint inventory (the probe families behind
jspl), the Kasada comparison and the claim table in the author’s repository are paywalled and were not read; the Manus outcomes are as reported. Server-side scoring is invisible outside DataDome. Read it to diagnose your own blocks, or as a defender auditing what a tag sends./chromium/stealth route from Python Playwright over CDP, with the built-in residential proxy pinned to Italy, solveCaptchas=true, typing at 70 to 220 ms per character and always reusing browser.contexts[0], because the docs apply profiles only to the default context. On Esselunga (the F5 BIG-IP TS cookie appeared only on the stealth route) the solver produced a token in 5 of 7 runs, in 10 to 35 seconds; the other two sat 75 seconds on an empty checkbox. With a token the login POST went through, and then the site demanded phone 2FA. It kept demanding it with emulationOs=macos and even from the author’s own browser: once flagged, the account stayed flagged.The reCAPTCHA Enterprise result. Tigros sits behind Cloudflare, which issued no challenge, and uses invisible score-based reCAPTCHA Enterprise. With
solveCaptchas on, the login failed on every try (the review says Browserless is fixing it, and does not give the number of tries). With it off, the page minted its own token and the login returned 200, 2 of 2. Saving state was one CDP call, Browserless.saveProfile (25 cookies, one origin). A fresh browser loaded with that profile opened the shop logged in, judged by the app calling its cart endpoint rather than by the cookie jar, with the login from Ancona and the check from Milan. A 7-day Session API copy behaved the same, because the site never rotated its token in the window.What it cost, checked against the pricing page. A unit is up to 30 seconds of browser time; residential proxy is 6 units per MB; a successful solve is 10 units and failed attempts are free. A login-state check cost about 8 units; the cart tests after the upgrade cost 914, mostly image-heavy pages over the proxy. At the Prototyping plan’s $25 for 20,000 units (annual billing) that is about a cent per check and $1.14 for the cart work; the author’s first saving would be blocking images.
The caveat. A review of a vendor’s tool, not a benchmark: its links carry tool-review tracking tags, and no sponsorship statement appears either way. Runs are tiny (7 and 2), on two sites in one country, and the Esselunga checkbox CAPTCHA is never named. The Linux-identity explanation for 2FA is the author’s hypothesis. Against the guide’s card on saved logins reverting on restart, this confirms the restore half and leaves token rotation untested.
httpx (one run, 3 Oct 2026) gave it a full page on 375 sites against 378 for a named bot string with a contact URL and email. Only the library default lagged, at 353. What Chrome’s string changed was which sites worked.httpx 0.28: the library default, a copied Chrome 140 string, MyScraper/1.0, Chrome with an email appended, and MyScraperBot/1.0 (https://example.com/bot; you@example.com) httpx/0.28. A success was a 200 carrying at least half the visible words of the best page any of the five received. Of 402 sites where someone got a full page: default 353, Chrome 375, made-up name 377, descriptive 378, Chrome plus email 380.The totals hide a swap. 23 sites served the descriptive string but not Chrome’s, 20 the reverse. Chrome lost on Wikipedia and Wikimedia, W3C, SourceForge, MySQL and LG (403s), Facebook, WhatsApp and Meta domains (400s) and Amazon’s country stores (202, empty body); Uber sent a 406 to every string except the descriptive one. Chrome won mostly where Google, Yahoo Japan and Mail.ru serve browsers a fuller variant (Google: 71 words against 33), plus Lowe’s and DigiCert, and on Lowe’s adding an email to Chrome’s string lost the full page again. Le Monde gave 11,736 words to both non-browser names and 1,547 to Chrome and the default.
Honest limits. One run, one location (IP type unstated), homepages only, and a sample pre-filtered to sites that already admit plain requests, so the hard tail is absent by construction. The five strings always ran in the same order through one shared client, so later ones may have carried cookies set by earlier ones. Reproducing the published counts from the released results needed a 50-word floor on the best page that the article does not state, and the 2-to-5-site gaps among the four non-default strings are single-run noise.
Where it sits in this guide. It complicates, not contradicts, the Bamberg result that two header edits clear 75% of headless-only blocks: there the edit removed
HeadlessChrome from a real Chrome whose handshake already matched; here a Chrome string rides Python’s TLS, the incoherence #detect warns about, and it earned nothing on average. It corroborates the #legal-ethics line that declaring yourself is often less detectable than hiding. The Web Bot Auth architecture draft the article cites, revised March 2026, is an individual submission that has since expired.X-AI header on only 572 hosts. A one-run recheck of 968 Tranco top-1,000 homepages found no such header at all.Where they sit, and the 87% this guide had not traced. Headers carry 7,887 (
X-AI 84%, X-LLM 13%); structured data 1,996, 26% of body injections, mostly JSON-LD. Both circulating figures are in the paper: 70% are non-visible by construction (headers, comments, metadata), and 87% are non-visible once rendering tricks such as matching colours, occlusion and zero-size elements are counted. 53% sit in headers or early HTML, where a model reads first.What changes compliance. In 5,200 hand-checked summarisation runs on 13 models, plain text stripped with BeautifulSoup was followed 3.9% of the time, HTML and a Playwright snapshot 1.1% each, the raw response 0.2%; small models reached 8.0% on text. But HTML and raw inputs errored in 20.3% and 25.8% of runs from context overflow, so part of their low rate is models failing before answering. Six times a model flagged the injection and complied anyway.
The caveats. A phrase-list scan is a lower bound: crawlable, English, unobfuscated text only, and summarisation was the only task tested. The top-1,000 recheck comes from a developer-marketing blog, one run from Mumbai on 3 October 2026: 633 of 968 sites answered, none sent an
X-AI or X-LLM header, and one homepage (Cloudinary) hid an sr-only note telling agents to use its API instead of the sign-up form. The “7,000 from site owners” line in circulation is data protection plus bot identification by purpose (7,189), not authorship.httpx and once in headless Chromium, same Chrome user agent. Of 544 real homepages, 62% had their text in the plain HTML, 3% as JSON in the source, and only 5% needed JavaScript. Blocking mattered more: 14% stopped the plain request but loaded in the browser.httpx GET and one Playwright Chromium load (DOM ready, up to 8 seconds of network idle, one more second), both sending a Chrome 140 user agent. 32 sites whose robots.txt disallows everyone were skipped; 279 unreachable, 112 with no real homepage and 33 browser failures or off-site redirects left 544. “Text in the plain HTML” means at least 60% of the words the browser showed were in the raw response; moving that line from 50% to 80% moved the share from 56% to 63%, and the JavaScript share stayed at 5%. The published CSV, tallied here, gives 335 / 18 / 8 / 29 / 77 / 26 / 51.What the blocks were. Cloudflare challenges accounted for 41 of the 77 plain-only blocks. All 12 Amazon stores served a challenge to the plain request: in the CSV, nine returned a 202 and three an empty 200, zero words each. Wikipedia’s 403 cleared once the user agent carried contact details, as its bot policy asks. The reverse group of 26 includes eBay, Zillow (a
px-captcha page) and Salesforce; the 51 that blocked both include The New York Times (DataDome) and Stack Overflow (Cloudflare).Speed and frameworks. On the 390 homepages with content, median time was 0.7 seconds for the plain request and 9.5 for the browser, recomputed here from the CSV, about 14 times longer, although the browser figure includes a load wait a tuned scraper would trim. 54 of 62 Next.js homepages were server-rendered, and about 55% of readable pages carried JSON-LD.
The caveats. Homepages only, one run, from Mumbai, with the network type unstated. One client of each kind:
httpx sends a Python TLS fingerprint under a Chrome user agent, so “plain request blocked” partly measures that mismatch, not plain HTTP in general. Word overlap is not meaning. The author writes developer marketing for web-data firms. Figures in circulation of 59% and 17% predate a 2 October correction: Cloudflare’s challenge script also sits on pages it does not block, so 14 sites had been misfiled as blocked.localStorage UUID, or else only if GPU, display and CPU–memory all match exactly one device. IP cannot link.fsid, plus 21 detection booleans shipped as default rules, among them hasWebdriver and hasCDP at 15 points each, hasUTCTimezone at 8 and hasSwiftshaderRenderer at 3. Mouse, keys and scrolls arrive in heartbeats every 30 seconds; clicks, form submits and copy or paste events go immediately. One default periodic rule flags more than three copies and three pastes in a session. Scores add up to a cap of 100.How sessions become devices. The docs say plainly that
fsid is not identity: one randomised canvas value changes it. So v1.3, released on 3 October, two days after the post’s v1.2, resolves devices in order. A known client UUID, kept in localStorage as ofm_device_id, links at once. Otherwise three groups must agree with exactly one stored device: a recognised hardware GPU renderer or UA-CH model; screen width, height and depths; and CPU count with device memory. Software renderers such as SwiftShader never qualify. Timezone, language, canvas, behaviour and “IP proximity cannot establish or rescue a hardware match.” Zero or several candidates means a new device.What it means for a collector. A rotating proxy pool does nothing here; a reused profile with its storage intact links every session to one device. With storage cleared, a fleet of identical cloud VMs looks like one machine wherever the renderer is recognised, and the docs list that collision as a known limitation. Investigators then pivot in a STIX 2.1 graph from session to IP, autonomous system and user agent, where clusters show up even when the device link fails.
The honest caveat. This is a one-author project with 8 stars, created in April 2026, and it publishes no accuracy or false-positive figure. Its 0.95 match confidence is, in its own words, “not a calibrated physical-device probability.” The stricter matcher is v1.3; v1.2 was looser, and v1.3 ships a tool to rebuild what it calls contaminated clusters. Visitors get a dismissible notice that is “not a consent gate”, and the docs flag GDPR exposure. It shows the design one defender chose, not what commercial vendors do.
The model, checked. Moondream 3 Preview appeared on 18 September 2025: 9B parameters as a mixture of experts, 64 experts with 8 active per token, about 2B active, 32K context, under the Business Source License 1.1 with an additional use grant. Moondream 3.1 followed on 7 July 2026 with open weights under the Moondream Model License 1.0, which permits self-hosting and fine-tuning and bars only general-purpose hosted inference. Neither licence mentions CAPTCHAs. The post does not say which release was used.
What it means for defenders. Image CAPTCHAs rely on rotation: new variants arrive faster than solvers can retrain. The author’s own forecast is that once variants rotate every few minutes and never return, zero-shot becomes mandatory, and a small local model would answer that without the retraining loop. That pushes defence toward what he credits hCaptcha with: a WASM virtual machine for obfuscation and fingerprint probes that flag anti-detect browsers. The weakest part, in his view, is the mouse and touch motion model. It is the same shift as the guide’s liveness section, away from the answer and toward how it was produced.
The honest caveat. Every number is the poster’s own, unpublished and unreviewed. “Zero-shot” is undefined; we do not know whether test classes were truly absent from training, whether 90% is per image or per challenge, or what share of challenges pass when several answers must all be right. No latency or hardware is given. Collecting 200k images from a live CAPTCHA service raises its own terms-of-service questions. This is one student’s report with one appreciative comment.
invisible_playwright (3,009 stars, created May 2026) drives a Firefox 151 build patched in C++ and derives every fingerprint surface from one reproducible seed. A hands-on review loaded one Cloudflare, Akamai, DataDome, PerimeterX and Kasada site with it and passed all five. The control is the story: headful vanilla Playwright passed four. Only one Kasada page separated them.feder-cr/invisible_playwright was created on 13 May, has 3,009 stars and 296 forks, and is not archived. PyPI shows 66 releases since 25 July, 38 of them in the last 30 days; 0.26.0 shipped on 5 Oct. The engine is a mozilla-central fork, feder-cr/firefox_antidetect_patch, publishing Firefox 151.0 builds for Windows x86_64 and Linux x86_64/arm64 only, with no macOS; fetch pulls a sha256-checked 240–262 MB archive. The licence is not plain MIT: pyproject.toml declares MIT AND Apache-2.0 because it ships a fork of Playwright’s Python client, and the binary is MPL-2.0. One account wrote 663 of 728 commits.What it patches, per its README. UA,
navigator and client hints are derived together; Accept-Language and navigator.languages are one declaration; canvas is deterministic per seed; WebGL is a validated persona whose ~81 getParameter values travel with the renderer name, so pinning an unknown GPU raises an error. Fonts ship with the browser, the codec table is declared, WebRTC offers one synthetic candidate carrying the proxy exit, and timezone comes from the egress IP. Those are the cross-surface leaks the anti-mage card names, such as Linux fonts under a Windows claim, handled in the engine. Nobody has published an anti-mage score for it.What the review measured. The Web Scraping Club review (Antonello Zanini, 4 Oct, free to read) reports reCAPTCHA v3 0.9 on AntCpt, APIVoid risk 0, CleanTalk 100/100 and a Pixelscan pass. Its anti-bot table used one unnamed site per vendor from a home residential IP. Vanilla Playwright headless failed all five, so most of that gap is the headless tell. Headful vanilla passed Cloudflare, Akamai, DataDome and PerimeterX and failed only Kasada;
invisible_playwright passed all five either way. The July fifteen-engine benchmark in this guide, covering the JS surface only, also scored it 100.The honest caveat. That is n = 1 per vendor, unnamed sites, no repeat count and no definition of “loaded”. The review’s claim that Firefox is the only engine patchable at binary level is wrong: CloakBrowser patches Chromium in C++. Firefox’s small share can itself be a signal on Chrome-heavy targets. With one main maintainer and near-daily releases, pin exact versions. This guide tested nothing.
Oct 2026 update. A 7 Oct practitioner carousel repeated this review’s figures, reCAPTCHA v3 0.9 and 5/5 sites, without the headful control, and its “nothing to read, no seam” outruns the evidence: CloakBrowser, also patched in C++ and also reported going 0.1 to 0.9 on reCAPTCHA v3, still scored 70 on coherence. Its MCP mode is a separate project, AIHawk. On 12 Oct: 3,117 stars, PyPI 0.35.0 and 77 releases in all, five of them in the last three days.
The numbers worth keeping. Crawlbase lists 9 tools in v1.3.0 (3 crawl, 6 storage), Jina 7, Firecrawl 26 in its full profile and ScraperAPI 28. Bright Data’s count is left as “broadest”, with
GROUPS and TOOLS settings to load a subset. Firecrawl’s README, which we checked, names about two dozen firecrawl_ tools, monitor and research among them. Playwright MCP is local, does no unblocking, needs your own proxies, and its README points coding agents to the Playwright CLI to save tokens. On billing, Bright Data gives 5,000 free requests a month. Crawlbase says its failed requests are not billed, and the other eight entries say nothing on failure billing. Scrapy MCP appears as a sidebar: it fetches nothing and connects an agent to a running crawl.What it adds here. This guide already shows that tool definitions cost tokens on every turn. This list shows the spread between scraping servers is about fourfold in tool count alone, and that you can trim it on some servers. Its security note is sound: a hosted server keeps your key in a config file (ScrapingBee’s sits in the endpoint URL), and the model can request any URL, so use a token you can rotate and watch the request log. It says nothing about prompt injection from fetched pages, which is the larger agent risk.
The honest caveat. This is a vendor ranking that puts its own product first and carries sign-up calls to action. It is a documentation review: no success rates, no head-to-head on shared URLs, and no token cost per server, so the tool count is a proxy for context cost, not a measurement. Counts change between versions. The no-charge-on-failure claim is Crawlbase describing itself, and we did not check any pricing page. Of the tool counts, we checked only Firecrawl’s against a primary source. The recommendation the article ends on, a 20–50 URL bake-off, is the right one.
{{variables}} filled from upstream. Parser turns HTML (CSS or XPath) or JSON (JSONPath) into named fields, with an optional regex clip and a default. Follow links takes allow/deny regexes. Pagination handles a next link, a ?page= parameter, a JSON cursor or an incrementing variable. Branch routes on exists, equals, contains, gt, lt or regex. Output writes JSON, JSONL or CSV. Values travel along edges as one flat dictionary of variables. The stack is a FastAPI backend, a React Flow editor and a CLI with validate, run and serve.How it maps onto Scrapy. Input nodes become
start_requests, Request nodes become scrapy.Request with the proxy in meta, and each Parser emits one new context per record. Scrapy supplies the crawl loop, retries and middleware. Global settings map onto Scrapy settings, and the model defaults are polite: robots.txt obeyed, 8 concurrent requests, 4 per domain, and a randomised 0.5-second delay. The JSON loader strips a )]}' guard prefix and unwraps JSONP, a small touch that shows API-scraping habits. Note what it is not: an interpreter, not a code generator. There is no spider file to export and maintain without the tool.Where it fits. The audience is someone who wants Scrapy’s scheduler without writing a spider, for list-detail-paginate jobs on unprotected sites, with the graph saved as a versionable JSON file. That is a different bet from prompt-to-scraper builders: the selectors are ones you chose and can read, so a broken field is a visible edit, not a regeneration. The README also says output files are written when the crawl ends, so a long crawl that dies midway may leave nothing on disk, which is our reading of the docs, not something we tested.
The honest caveat. This is a two-day-old personal launch. The GitHub API on 5 October 2026 showed creation on 3 October (first commit 2 October), 7 commits, 0 stars, 0 forks, no release and no licence, which under default copyright means no permission to reuse the code. There is one test file with nine tests. Examples run against quotes.toscrape.com and a bundled fake storefront. There is no browser rendering and no TLS impersonation, so protected targets are out of scope, and the author calls it a first base version. We read the README and engine source but did not run it.
What Cloudflare published on 15 September. A new Training option, Disallow AI Training, publishes a no-training preference in robots.txt, keeps crawlers Cloudflare calls Accountable allowed for search, and blocks every other training crawler, including the training-only crawlers of Amazon, Anthropic, Meta and OpenAI. Block and Block on pages with ads now apply to mixed-use crawlers, Googlebot included. The migration: a never-configured domain with the legacy toggle off goes to Allow on all three; legacy Block goes to Search Allow, Training Disallow, Agent Block on pages with ads; earlier Training blocks become Disallow. Block AI bots and Managed robots.txt are deprecated.
What it means in practice. Through a migrated setting Googlebot keeps crawling for search, and the training opt-out travels through
Google-Extended in robots.txt. The edge-block mechanics in the warning stay true for anyone who now picks Block, and Cloudflare says so outright. Comparing origin-log Googlebot volume either side of the 15th is still a cheap check, though Cloudflare’s own security events show blocks directly. For collectors, the pass is narrow: a training crawler without the Accountable designation is blocked, not asked.The honest caveat. This is Cloudflare describing its own product; no zone was observed. Its Block AI Bots docs page, last updated 1 July, still carries the July wording, so Cloudflare’s sources disagree until the docs catch up; the dated 15 September post is the newer statement. We found no primary Cloudflare page for the “every existing free-tier site” wording, and the migration table maps untouched domains to Allow. The warning itself carried no data or links. Bing’s robots.txt no-training support is only targeted for early 2027.
403, 5.7k attributed to OAI-SearchBot, 41.61 MB of HTML served and 10 referral visitors. Comparing source IPs with OpenAI’s ranges, the publisher found only 22% inside them and called the other 78% bots pretending to be OpenAI. He said the dashboard numbers are not accurate and started a week-long controlled test; no result is published yet.Why the tile says OpenAI. Cloudflare documents it. On the free plan, AI Crawl Control identifies AI crawlers by user-agent string, and the more thorough detection via Bot Management detection IDs sits on Enterprise plans. Its GraphQL reference marks the user-agent filter “Can be spoofed” and the detection-ID filter “Reliably verified by Cloudflare”, and its bot reference lists two detection IDs for each OpenAI crawler. So on a plan without Bot Management, an OpenAI tile can plausibly count anything that types
GPTBot, ChatGPT-User or OAI-SearchBot. We do not know which plan this site runs.How to check it yourself. OpenAI publishes four feeds, all checked on 30 Sep 2026:
openai.com/gptbot.json (18 prefixes, generated 22 Sep), chatgpt-user.json (230 prefixes, 25 Sep), searchbot.json (39 prefixes, dated 2 Jan) and adsbot.json (2 prefixes). Match the user-agent token, then require the source IP to sit inside that token’s own feed, fetched fresh; a GPTBot string from a ChatGPT-User range is still a mismatch worth logging. Do it on raw edge logs with client IPs, not on the dashboard’s aggregates.The honest caveat. This is one site, one screenshot, and an unpublished comparison: the window, the feeds used and how IPv6 was handled are all unknown. A commenter argued OpenAI may send traffic from infrastructure outside its declared ranges; OpenAI’s bots page lists only these feeds, so unmatched traffic is best called unverified rather than proven fake. A stale copy of a feed also overstates spoofing. Do not generalise the 78%; the durable point is that the label and the evidence are different things.
402 Payment Required with a crawler-price header; a crawler agrees with crawler-exact-price or pre-authorises with crawler-max-price, and the 200 comes back with crawler-charged. The catch: only crawlers registered with Cloudflare and signing with Web Bot Auth can pay. For everyone else a 402 is a block with a price on it.402 plus crawler-price: USD 0.01, resend with a matching crawler-exact-price. Proactive: send crawler-max-price first and be served if the price is at or below it. Only one price header per request, and it must be among the components signed in signature-input. Failures carry a crawler-error header with one of eleven codes, from MissingCrawlerPrice (402) to CrawlerForbidden (403), StrongAuthRequired (400) and InvalidContentPrice (502). Error responses are not billed; every successful re-crawl is. /robots.txt, /sitemap.xml, /security.txt and /crawlers.json stay free.What changed since the July 2025 launch. It launched as one flat domain-wide price in private beta, and the docs still say closed beta. A June 2026 changelog added dynamic pricing: the origin may return its own
crawler-price, and Cloudflare tags origin requests with cf-pay-per-crawl: protocol=cloudflare, pricing=in-band (or zone-default, bypass); URI patterns can be exempted. Settlement runs through Stripe with Cloudflare as merchant of record, a signed Discovery API lists charging domains, and Cloudflare sets no spending limit for the crawler. WAF and Bot Management blocks override Charge.x402 is a second, different 402. Cloudflare’s July 2026 Monetization Gateway, a waitlist so far, would let a site charge any caller for a page, dataset, API or MCP tool over x402, settling in stablecoins (USDC, Open USD) with no account, via per-route rules such as $0.01 per call to
/api/premium/*, variable prices up to $2, or origin 401s turned into 402s. The post names no headers; the x402 spec returns a base64 PAYMENT-REQUIRED header, the client resends PAYMENT-SIGNATURE, and success carries PAYMENT-RESPONSE. A handler has to tell the two dialects apart.The honest caveat. Everything here is vendor documentation; we saw no 402 in the wild and tested nothing. Neither product publishes adoption numbers. “The bot pays and gets the page” holds only for a registered, signing crawler with Stripe connected; an unsigned
crawler-max-price is simply rejected. Give no automated client the ability to pay without a hard budget.Oct 2026 update. The founder of ProxyScrape, a proxy seller, objects to the model itself: a crawler that must fetch 999 low-value pages to find one pays for all 1,000, so per-page pricing only fits sites whose data is already structured. Replies split: on data-dense sites such as job boards most pages carry value and filtering is the buyer’s job; another proposed a free index of each paid page’s contents, date and price, plus a changed-data feed. Per-URI pricing and exemptions answer part of this; the docs offer no such index. Opinion, no data.
<lid:n>, with navigation and footers left in. The model writes a short program: an <extract> list of line removals (rm 1-18), then one tag, keep, edit (remove more lines or exact substrings), delete, or rewrite. Plain code executes it, so every operation except rewrite only deletes and kept text is copied, never regenerated. The model was distilled from three teachers: Dripper (a 0.6B extractor), Qwen3.8-27B for refinement, and a 1B rephraser for deleted pages that still carry educational value. Extraction alone removed 58% of tokens, mostly at the page edges.The numbers. DCLM Core (22 tasks): 0.1535 / 0.2735 / 0.3184 against the best baselines of 0.1465 / 0.2615 / 0.3068 at 400M / 1.4B / 2.8B. With four scrapers (resiliparse, trafilatura, jusText, Dripper) each followed by RefinedWeb rules, the four stayed within 0.003 Core of each other at 1.4B, and ReScraper beat its own teacher Dripper by 5.8% and 8.3%. Cost: 494 H200 GPU-hours for the pool, compared with 1,366 for Dripper alone. That is roughly 36,000 pages per GPU-hour (this guide’s arithmetic). Over five seeds at 400M it scored 0.154 ± 0.004, against 0.142 ± 0.004 for UltraX.
Why a scraper should care. In the paper’s examples, resiliparse fused table cells (“225 225” became “225225”) and dropped a documentation page’s bulleted list. No refiner working downstream could restore either. The line rendering kept both. The reusable pattern is the output contract: the model emits deletions addressed by line id and code applies them, which keeps an LLM out of the business of retyping your content.
The caveats. This is a v1 preprint (28 Sep 2026) and has not been peer reviewed. Success is downstream pretraining score, not extraction accuracy against a gold set, and its 89.3 token F1 is measured against its own teacher. It handles English pages only, skips any page over 29,696 tokens, and decodes at temperature 1.0, so reruns differ. Rewrite writes new text (mean BERTScore 0.89 against source), so drop it for anything that must be verbatim. The main table is one seed per scale. Everything is released under Apache-2.0 (model, code, 7.44B-token corpus). The repo is three days old with 3 stars.
Why AI stays out of the request path. Oxylabs puts its traffic at about 7 billion pages a day. Parsing 10% of that with the cheapest LLM at about a cent a page would cost roughly $7M a day. Zyte’s R&D speaker said the job needs a 99% solution, not 85%. The counter-case came from Diffbot: a twentieth to a tenth of a cent per page, but on cleaned article text, not raw HTML. Scrape.do says more than 99% of its requests run without a browser.
What the cited papers say (checked here). NEXT-EVAL (164 pages, Gemini 2.5 Pro): F1 0.96 on Flat JSON, 0.10 on slimmed HTML with a 0.91 hallucination rate. This guide’s own “F1 > 0.95” line leaves that condition out. Odysseys: on 200 live multi-site tasks the best agent (Opus 4.6) was fully correct on 44.5%, at about 30 minutes and $2.50 per 100-step task. A 1.2B-URL scan validated 15,387 prompt injections on 11,722 pages. Crawlers and scrapers were the dominant target, and about 70% of injections sat in HTML that never renders.
The caveat. The author works in developer marketing for web-data companies, and most talks are vendors describing their own systems; the post itself says none of the numbers has been tested independently. This guide read the synthesis and four of its papers but did not watch the talks, so every Oxylabs, Zyte, Diffbot and Scrape.do figure is a secondhand quote from one company each. The synthesis also says 87% of injections were invisible to humans. Traced since (Oct 2026): the CISPA paper itself (arXiv 2604.27202) reports both, 87% non-visible to humans overall and about 70% non-visible by construction in non-rendered HTML. Treat the synthesis as a map of where to look, not as evidence.
Oct 2026 update. A dedicated write-up of the talk clarifies the figures. The 51% was the agent’s share of all parser changes while an OKR targeted it; 14% came after the OKR ended. The 59% and 87% are acceptance rates for each side’s own merge requests, and median time to production was 7 days against 20 hours. The 5-to-2-hour fix time belongs to the assistive second agent, not a separate one; its stated guardrail is a 95% significance level; it gives no 30-job figure. The paper gives 87% non-visible, 70% by construction.
scrapingarena/scrapers, MIT, created 13 August 2026) is committed to by Tadas Gedgaudas, who announced the result, and the site header reads “ScrapingArena by appkittie.com”. The corpus is 100 URLs on 100 domains: Akamai 27, Cloudflare 24, unknown 17, DataDome 16, HUMAN 10, Fastly 4, Kasada 1, Imperva 1. Each target gets up to four attempts at concurrency 1, and success is decided by a shared validator ending in an OpenAI model. Every scraper runs direct, from GitHub-hosted runners, and through proxies; published NodeMaven runs use its -filter-medium IP filter, the “filtering technology” in the claim.What the raw run says. Run 36417282715-3 matches the leaderboard image on every speed figure we checked. Camoufox was 55 NodeMaven, 50 direct, 45 Oxylabs: five targets apart in one run. The top results were direct: ShardBrowser 66, Steel 58, Fortress 55 (43 NodeMaven, 39 Oxylabs). Where NodeMaven clearly beat Oxylabs was curl_cffi, 46 against 33, Cloudflare 15 against 6 of 24. The grid is incomplete: ShardBrowser and Moli had no NodeMaven row, curl_cffi and Patchright no direct row. Steel through either proxy fell to 5 or 6 with zero blocks recorded, which reads as an integration fault, not a verdict.
The Google claim. Camoufox + NodeMaven did pass
google.com/search on the first attempt with no cookies. So did Camoufox + Oxylabs on the second, and five browsers running direct; CloakBrowser + NodeMaven failed it. One URL in one run is not evidence for a provider. The per-vendor “best against” figures sit on tiny strata: 75% against Fastly is 3 of 4.Caveats, and who pays. NodeMaven and Oxylabs both appear in slots labelled Sponsored, with links tagged
sponsor_ads, the site sells weekly placements, and the announcement invites providers to integrate. Whether inclusion is paid is not stated. One run, one US region; credit the open JSON, which made this check possible. The table was transcribed from small text, though every figure checked matched.localStorage and IndexedDB; storageState keeps IndexedDB only with indexedDB: true (Playwright 1.51+); a token rotated Wednesday reverts when Thursday’s worker restores Monday’s file.localStorage tokens, IndexedDB values and OAuth temporary tokens, each on its own eviction clock. A cookie can outlive the server session; an access token can expire while the refresh token works; a rotated refresh token dies when the new one issues. So a snapshot is “a photo of your login on the day you took it”, and a restart after rotation restores a dead token. Checked independently: Playwright’s v1.51.0 notes add the indexedDB option to storageState(), naming Firebase Authentication; older versions silently drop those tokens.Detect, then recover deliberately. Auth failures rarely throw: a 302 to
/login, a 200 that is a sign-in page, a 401, a verification flow. After restoring state, check one or two strong in-app signals (URL not on the login route, an account-only element present), and keep that separate from ordinary failure so a 500, a timeout or a moved selector never triggers a login. On confirmed expiry, log in, then overwrite the snapshot so the next worker does not start from last Monday. Skip sessionStorage: it is tab-scoped, and replaying it can break OAuth redirects and CSRF. Snapshots give ten workers ten isolated copies; a live browser keeps rotated tokens but bets on Chrome surviving weeks of leaks, restarts and deploys. Production uses both.How it sits next to the guide. The G2 card counts anonymous session life in requests (21 to 106); the 30–50-request rotation in Browsers as a Session Layer is for minted anonymous cookies, not an account you keep for weeks. And a logged-in session is one you accepted terms to obtain: the guide’s cases put hiQ’s loss and Power Ventures’ post-revocation access on exactly that ground, so an automated re-login loop needs stop conditions, not just retries.
The caveat. Vendor experience, not measurement: no failure rates, session lifetimes or target counts. It closes on Browserless’s own Authenticated Profiles and Session API, matching its docs. That keep-alive needs
browser.disconnect(), absent in Playwright, is the author’s claim. The one comment is from Kameleo, also a vendor.OfflineAudioContext render read through getChannelData() runs silently on any page, while the raw microphone path through getUserMedia() always needs the user’s permission.OfflineAudioContext, feeds an oscillator through a DynamicsCompressorNode, reads 32-bit float samples with getChannelData() and hashes them. Fingerprint’s technical write-up attributes the differences to engine lineage, platform-specific code such as Chrome’s separate FFT on macOS, and floating-point behaviour; on one device it measured Chrome 101.46, Safari 79.59, Firefox 80.95, in 5 to 50 ms, with no permission. The essay’s claim that CPU architecture, floating point and SIMD can shape the output fits that, and it hedges with “potentially”, as it should.The prompted path.
autoGainControl, noiseSuppression and echoCancellation are real audio constraints, and switching them off brings the stream closer to raw input. But getUserMedia() works only in a secure context and, per MDN, “must always get user permission”; a refusal throws NotAllowedError. An anti-bot cannot sample your microphone silently. The author’s own correction is right too: this is not reading the ADC, because browser, OS media stack and driver sit in between. It matters on liveness and verification flows, where the stream itself is judged.The Google study, scoped. The unlinked reference to Google research on real Chrome clients is Bacis et al., Assessing Web Fingerprinting Risk (WWW 2024, arXiv 2403.15607): tens of millions of Chrome instances on hundreds of thousands of sites, 161 Web APIs including
getChannelData, sampleRate and hardwareConcurrency. Its point is that correlated surfaces add less joint entropy than their sum, and they cluster. Our reading, not the paper’s: a value that breaks its cluster stands out more than an unusual but coherent one.What this is. An exploratory essay: no measurements, no code, no links. Treat its stack model as a map, not a finding. Real users are noisy too: Safari 17 adds audio noise in Private Browsing, Brave randomises output and Tor disables Web Audio, so an odd hash is weak evidence and an incoherent one is strong.
The argument, applied to scraping. A GCC Data Protection newsletter (15 Sep 2026) takes one case: a self-styled magazine reposted a data expert’s bio and photo without asking. Article 10 opens the door to collection; using the data still needs a basis. Consent is unworkable at scraping scale; legitimate interest under Article 16 of the Implementing Regulations needs a lawful purpose, balanced interests, no sensitive data, the person’s reasonable expectations, and a documented prior assessment. Article 12 requires a privacy policy first. Its sharpest point: a public figure’s page can carry health details, and a scraper that reads a field to discard it has arguably processed it.
Cross-border enforcement is weak, not absent. The article expects nothing to happen, as the publisher appears to be in Pakistan, and points to Clearview AI. Europe’s record: Italy €20m (2022), Greece €20m (2022), France €20m (2022) plus a €5.2m overdue penalty (2023), the Netherlands €30.5m (2024). The UK ICO’s £7.55m fine was quashed on jurisdiction in 2023; the Upper Tribunal held in Oct 2025 that UK GDPR applies and sent the case back. Reports say the EU fines went unpaid, and noyb filed a criminal complaint in Austria on 28 Oct 2025 naming executives. Hard to collect, but the exposure follows the company.
Caveats. Not legal advice. One practitioner’s newsletter, one anecdote; the “processing by looking” point is interpretation, not SDAIA guidance. SDAIA’s PDFs refused our requests; wording was checked against reproductions and law-firm summaries. A&O Shearman counts 48 PDPL enforcement decisions in a year to Jan 2026; we found none against a foreign scraper.
The numbers. 13 Aug–6 Oct 2024; 34 bots (7, 8 and 19 per state), online 49 days on average; 229,807 videos. Party accounts made up 0.18% of feeds, about one party video per 516 minutes of scrolling, but AfD appeared at 5.46 per 10,000 and BSW 5.30 against SPD 1.32 and CDU 1.57: a 3–4× gap for new, neutral users. A 2025 follow-up with Bertelsmann Stiftung scaled to 268 profiles, four platforms and 2.6 million videos.
Where the law now sits. Article 40(12) of the EU Digital Services Act obliges very large platforms to give researchers access to publicly accessible data for EU systemic-risk research. On 5 Dec 2025 the Commission fined X €120m, partly because its terms barred independent collection such as scraping; X’s corrective plan reportedly commits to saying eligible researchers cannot be contractually barred from it. Vetted access to non-public data opened on 29 Oct 2025 under Delegated Regulation (EU) 2025/2050; one documented early request was rejected on five grounds. The ESMD card covers the other end: how to share what you collected.
Caveats. The keynote is on 1 Oct 2026 and the programme gives no abstract; we have not seen it and built this card on the team’s paper and the DSA record. The paper is a preprint; party exposure is inferred from accounts and hashtags. Its claim that TikTok does not prohibit bots in principle is unverified by us: check current terms. Article 40(12) does not clearly cover automated fake accounts, and it does not cover commercial collection. Not legal advice.
Dedup the fetch, keep the repost. Its exact-match step hashes stripped, lowercased text with SHA-256; near-duplicates go through
all-MiniLM-L6-v2 embeddings at a 0.6 cosine threshold, flagging toy pairs at 0.72 and 0.81. The sharper point is the distinction: fetching the same post twice is a data-quality bug, while independent people reposting it is evidence of spread, so dedup collection on the platform post ID, not on content. Relative stamps such as “3h” must be resolved against scrape time; a consequence the tutorial does not spell out is that the displayed value is truncated, so “2d” carries up to a day of error, harmless in daily windows and wrong in hourly ones. Its resolver handles only minutes, hours and days.The legal footing, briefly. The tutorial is direct that most platforms ban automated access in their terms, that GDPR does not exempt data because it was public, and that handles, bios and locations should be anonymised. LinkedIn’s own
robots.txt opens by prohibiting automated access without express permission. The guide’s cases put the safer ground at logged-out public data. One line to reject: that buying from a social-data provider means you need not manage extraction legality; the article’s own summary says a provider does not remove your responsibilities for what you collect and store.The caveat. A tutorial, not a measurement: two sponsor blocks, five-post examples, no platform-specific extractor, no figures for block rates, lead time or accuracy. The 0.6 threshold is illustrative; the API remarks are qualitative. Not legal advice.
Oct 2026 update. The same author’s market-data piece (Web Scraping Club, 11 Oct; an explainer, no measurements) adds the high-frequency version. Never deduplicate on value, since two trades can share a price: key on a source event ID. Expect late and out-of-order events, and decide before launch whether a published minute bar is corrected, versioned or flagged. Buffer through a stream, not straight into the database. And a price you can see may still bar storage, model training or redistribution.
Why stealth backfired. Crawl4AI’s stealth mode added HTTP header variation that its normal mode lacks, and it reported the notifications permission as
"default", which is not a standard permission state. BrowserUse’s stealth mode ran through cloud infrastructure whose addresses Cloudflare and Google had already listed. Each patch added a new inconsistency. That is the anti-mage finding reached by a different method on a different population. Without stealth, OpenClaw and Claude for Chrome on Sonnet 4.5 got past every defence, Prosopo included,.The honest alternative, and its limit. In UC Davis’s FP-Agent tests, Cloudflare’s free AI-bot controls fully blocked only 1 of 7 agents, Manus, the one that identified itself. The rest got through and were also invisible to the site’s policy. The paper lists cryptographic attestation such as Web Bot Auth as a coming layer. A declared User-Agent token plus signed requests makes you legible. One practitioner saw a headed agent pass a Managed Challenge 5 of 6 times from home and 0 of 3 from a cloud server, so test from the network you deploy on. Identity is not access, though: a signature proves who you are, and the site still decides whether a small unknown operator gets in.
The caveat. One study, two Linux machines, Chrome and Firefox only, a four-week window. Canvas and behavioural signals were out of scope. “Stealth hurts” is shown for these tools’ stealth modes, not for every patch. One practitioner countered that correctly engineered patching does not backfire, and the guide’s coherence cards agree the failure is incoherence rather than patching itself. The 5/6 vs 0/3 split is n=1.
Oct 2026 update. The reverse case, n=1: a user was served a slider challenge for the first time on a site visited daily, while Meta’s Muse agent, which press reports say browses from a per-user cloud VM, reached the same site unchallenged. Site, vendor and timing are unknown; this is one false positive against a human, not a rate, and the agent may have been recognised rather than missed. Replies called slider checks security theatre now that bots solve them. Read “not challenged” as a site policy outcome, not proof of stealth.
What FP-Agent adds. UC Davis (Wang, Shafiq, Vekaria, arXiv 2605.01247) ran 1,000 trials each of Atlas, ChatGPT Agent, Claude for Chrome, Comet, Manus, Browser Use and Skyvern on a honey site, against 546 sessions from 56 human participants. Fifty behavioural features gave F1 0.9993; browser fingerprints alone gave 0.7969. Each agent types in its own way. Several paste, Claude fires programmatic change events, and Browser Use emits double the change events. All of them jump the pointer straight to the target.
The operating rule. One engineer logged an agent typing a phrase and clicking a button. The model paused for 4.7 s with no input, then the framework sent 15 keys about 8 ms apart and one straight move to near dead centre. Adding synthetic curves on top adds signals a model can check. Fewer UI actions leaves less to score: if you need the content, fetch and parse it rather than drive the page to it.
The caveat. Akamai’s work is a vendor-published proof of concept with a small labelled set, overlap with single-click humans, and a PR-AUC of 0.661 that is far from production-ready. FP-Agent is closed-world: it names known agents rather than spotting new ones. Its humans were undergraduates, and on held-out task types behavioural F1 fell to as low as 0.6303. The keystroke trace is n=1. The figure “0.9994” seen quoted elsewhere is 0.9993 in the paper.
tls-client’s chrome_152 profile matched Chrome 154 on JA4, groups, GREASE and HTTP/2. curl_cffi 0.16.3 (chrome150) and uTLS failed JA4, both lacking the new trust_anchors extension.The 26 Sep results. All four Chrome channels produced the same JA4 (
t13d1517h2_..., 17 extensions). tls-client v1.16.0 with chrome_152 passed all four checks on every channel. curl_cffi 0.16.3 with chrome150 passed groups and HTTP/2 but failed JA4 (16 extensions: missing 0xca34 trust_anchors) and GREASE (no GREASE in signature algorithms). uTLS v1.8.2 HelloChrome_Auto, which resolves to a Chrome 133 profile, missed the same two and also failed HTTP/2 with Go-default settings. Plain requests failed everything, as expected.Generated fixes. The site also builds candidate profiles from the latest measurement and retests them: a tls-client clone passes all four; a uTLS clone passes TLS but not HTTP/2; a curl_cffi custom-settings attempt got worse (12 extensions). That last result says patching curl_cffi from Python options is harder than switching profile. The practical reading for the guide’s profile-drift card: the drift is real and now quantified, and at this snapshot the gap is one extension and one GREASE slot rather than a wholesale mismatch, which is exactly the kind of small, silent miss a JA4 hash exposes and a human eyeballing headers does not.
Caveat. One author’s tracker, days old, macOS only, three runs at the time of reading. The four checks are the handshake layer only; header order, HTTP/3/QUIC and JS signals are out of scope by design. A pass here is necessary, not sufficient. A related Rust client (Phantom, pre-1.0, 6 stars) claims Chrome 154 parity including QUIC but is unmeasured by the tracker so far.
reese84 cookie and the incap_* jar, and replayed it with curl_cffi from an Irish residential IP, an Ohio datacenter IP and an Italian home line. All returned the real page. The server asked for renewal after renewInSec: 699.token plus "renewInSec":699. That token becomes the reese84 cookie. When Imperva falls back to a GeeTest challenge, a paid solver returned answers in about ten to twelve seconds at about $1 per thousand. With retries included, the author puts a warm-up at around a tenth of a cent.The replay result. The warmed jar (
incap_ses, reese84, incap_sh, visid_incap, nlbi) returned full HTML with no browser, through curl_cffi, from the Irish residential IP that created it, from a datacenter IP in Ohio and from the author’s own connection in Italy. Mismatched TLS fingerprints and a Firefox User-Agent also worked. The author’s summary is that the cookie acted as a bearer token, with nothing binding it to the client that earned it.What it changes here. The guide’s Imperva profile says datacenter IPs get blocked on sight. On this target, once the jar existed, they were not. The design that follows is the same one the /goto cards reached: do the expensive, stealthy step once per token, then spend the token from cheap infrastructure until
renewInSec runs out. The headline’s “thirteen minutes” is the author’s figure. The only number visible to us is 699 seconds, about 11.5 minutes.Caveat. This is one site, one practitioner, one write-up. The IP tests used three networks, not a sample, and no failure rate over time was published. The explanation of why the replay works sits behind a paywall and we have not read it. Imperva can bind tokens per customer configuration, so treat “no IP binding” as a property of this deployment on this date. Test your own target before you design around it.
minet, is not in the table at all.What the guide’s table says. Six of the seven have rows in the #libs table. Four carry star figures that are now well short: Scrapling 38k (live 84.2k, 2.2×), ScrapeGraphAI 18k (31.4k), Crawlee 15k (25.9k), Katana 8k (17.6k). Botasaurus and Trafilatura show a dash.
minet has no row: it is a research-oriented CLI for collecting from web pages and platform APIs, the only copyleft (GPL-3.0) licence of the seven, which matters if you embed rather than run it.The only soft signal. Botasaurus is the one repo not pushed in September; two months is not abandonment, but it is the one to recheck before relying on its anti-detection claims against current Chrome, since those decay fastest. Note too that Crawlee’s JS repo and its Python port are separate projects with separate rows and separate star counts; quoting the JS figure for the Python library, or vice versa, is an easy mistake.
What to do with the table. Either re-pull the star column from the API on a schedule and print the date beside it, or replace it with a last-push date, which is the number that actually warns you off a dead dependency.
Caveat. Stars measure attention, not fitness, and Scrapling’s curve in particular has been driven by social virality. This check says nothing about whether any tool clears a given anti-bot; it verifies only that each project is alive, what licence it ships under, and that the guide’s own numbers need a dated refresh. The source list was an engagement-driven roundup with no testing behind it.
Where the maps differ. His list has no slot for in-page JavaScript fingerprinting (L3) or hardware and OS side channels (L4). That fits his framing that most checks run before the page loads, and with the guide’s point that the server holds a full dossier before any script runs. So read the four as the pre-render front line, not the whole stack. The guide treats layers as scored together for coherence. He presents them as separate questions, though he agrees no single one decides. He also puts request rate under behaviour, where the guide splits it between L2 timing and L6.
What he adds. Because agents drive real browsers, all four checks weaken at once: the claim, the accent and often the address are genuine. Well-behaved agents have started signing requests (Web Bot Auth), but today the badge names the company that built the agent, not the person it acts for. The old question was “are you human?”. The new one is “who sent you?”, and there is no standard yet for a store to tell whose personal shopping agent is at checkout. The guide’s payment-rails cards (Visa Trusted Agent Protocol, Mastercard Agent Pay) are the nearest thing to an answer.
The caveat. This is one practitioner’s plain-language framing, written for a general audience. It has no numbers, no weights between the four checks, and no links. It records how one former insider organises the problem, not a measurement of how Cloudflare scores traffic today. A TikTok anti-scraping engineer endorsed the TLS-as-accent analogy in the comments. Nothing else was added or disputed.
.env files, and said the spoofed requests got 200s where his own plain request got a 404. One unverified log, no methodology — but the lesson holds: an AI crawler’s user agent is a claim, and the operators publish the evidence to check it..env files, which commonly hold API keys and database credentials. He reported that the spoofed requests received HTTP 200 while his own unadorned request for the same path got a 404, which suggests a rule somewhere was treating those user agents as trusted. That is n=1, from a log screenshot that was not transcribed, with no IP, dates or server config published; treat it as a plausible anecdote, not a measurement.How to check a claimed AI agent. Most major operators now publish machine-readable ranges. OpenAI serves
openai.com/chatgpt-user.json and searchbot.json (the ChatGPT-User feed was regenerated on 25 Sep 2026). Anthropic serves claude.com/crawling/bots.json covering ClaudeBot, Claude-User and Claude-SearchBot, after previously saying it did not publish ranges. Amazon publishes Amazonbot addresses on its developer site. The rule: match the user-agent token, then require the source IP to fall inside that operator’s current feed; anything else is an unverified claim. Where an operator signs requests with Web Bot Auth, a valid signature is stronger evidence than any IP list.The honest caveat. “It came from Google Cloud” is not proof of spoofing by itself. Anthropic’s own published list, checked on 28 Sep 2026, includes single
34.x and 35.x addresses that sit in Google Cloud space, so the only valid test is the exact published list, fetched fresh, not the cloud provider. We could not verify xAI’s or Moonshot’s (Kimi) verification method, and nothing here confirms who ran the probe. A promotional comment on the original thread pitched NoCapture, a screen-capture-blocking desktop product from Membran Digital; it is a vendor, and irrelevant to HTTP-level verification.Why scrapers should care. Borrowing a famous AI crawler’s user agent looks like a shortcut past allowlists, but it is now one of the easiest things for a defender to disprove: one lookup against a public JSON file. Once flagged, your traffic sits in the same bucket as credential-harvesting probes, which is reputationally and possibly legally toxic. If you need access, declare an honest user agent of your own or join a verified-bot or signing programme.
{"posts": [], "has_next_page": false} with a 200. One vendor founder traced three causes that look identical: a slug that did not resolve, a login wall served to datacenter IPs, and a rotated GraphQL doc_id. The check: an empty feed from a group that exists with members is a wall, not an empty group.Cause two: the login wall on public groups. A public group with over a million members also came back empty. What Facebook served an unrecognised visitor was a login page, not the group, and the author reports it does this far more readily to datacenter IPs. Because the page loads fine, the wall arrives as a 200 with no posts, not a 403. Cause three: doc_id rotation. Working scrapers load the page once for a token, then call the internal GraphQL feed query, which is addressed by a numeric persisted-query
doc_id rather than its name. That ID changes when the front end ships, and a scraper pinned to an old one gets empty or malformed responses, again without an error.The one check that separates them. Call the details lookup, confirm the group is public and has a non-zero member count, then page by cursor; if the very first page is empty on a live, populated group, raise instead of storing zero rows. The same session also hit a fourth, loud failure (connection refused for a period, on a plain Page too), which at least announces itself.
Caveats, and the legal edge. This is one morning, one person, written by a co-founder of a scraping API whose client the code uses, so the discrimination logic is the transferable part, not the product. No failure rates are given. Private groups are only served to logged-in members; tools that “scrape” them run your own session, which breaches Facebook’s terms and risks the account. Public-group scraping still sits under Meta’s terms contractually and under GDPR the moment you store author names and profile links, so collect posts, not member rosters.
The model that held. Treat the page number as part of the record’s state. The first time a record is seen, store its page, and never change it. When the same record shows up on an earlier page than its stored one, you have been bounced: paginate forward to the stored page and resume from the same row. The loop rule changes to match: same record on the same page with the listing not advancing is a loop; page 10 then page 1 with a known record is a bounce. Three pieces of state replaced seven, and each now lives where it belongs: on the record, not in a crawler-wide cursor that every bounce has to patch. If a fix needs seven flags, the model is probably wrong.
The test that proves it reproduces the failure. A fake listing of 2 pages × 10 records, bouncing to page 1 after every record: with the fix on, 20 of 20 records; with it off, the INFINITE_LOOP guard fires. A test that cannot reproduce the original failure does not prove the fix, and neither does a green run on a site that happened not to bounce that day.
Caveat: one engineer, one site, one synthetic test. No code, no production numbers, no comments corroborating it. The scheme assumes a stable record key and a stable ordering; if the listing reorders between visits, a record can legitimately appear on an earlier page and you will read churn as a bounce. It also costs one forward paginate per bounce, which on a deep listing is quadratic. Where the site exposes a direct page URL or keeps state in a cookie you can reset, jumping straight to the page is cheaper than walking forward.
TinyFish’s numbers. Lowest median cost on all four boards: $0.035 (Hard Retrieval search), $0.050 (search & fetch), $0.212 (Multi-hop search), $0.219 (Multi-hop search & fetch). On Hard Retrieval search & fetch it completes 79.0 ± 2.0% using a median 12,844 tokens, against Exa deep’s 83.0% at $0.127 and 23,660 tokens — a genuinely good trade: about 40% of the cost for 4 points less.
Where cheap costs you. Search-only Hard Retrieval: TinyFish 59.3% vs Perplexity 77.3% at $0.059. Multi-hop search-only: F1 26.6% vs Parallel basic 46.5% at $1.120. Multi-hop search & fetch: 30.2% vs Exa deep 48.2% at $0.683. Divide cost by success and on multi-hop TinyFish stays cheaper per correct answer, but you get roughly half as many correct answers; if a miss triggers a human or a retry, the ranking can reverse.
Caveats. The operator states no vendor pays for inclusion or ranking, but publishes no further conflict disclosure; the headline framing came from its own CEO and TinyFish thanked it publicly. n is small (45 multi-hop questions), task domains are narrow (coding docs, company discovery), and results depend on the fixed model and prompts. One benchmark, one snapshot; rerun on your own queries before switching providers. The page also runs a 300-question Factual Lookup board, which this spotlight left out, so check whether your workload looks more like lookup than deep retrieval.
What is older or cumulative. You.com’s $100M Series C at $1.5B was Sep 2025; Tavily’s $25M Aug 2025 (TechCrunch); TinyFish’s $47M Series A Aug 2025 (BusinessWire); Browserbase’s $40M Series B Jun 2025 ($67.5M total); Kernel’s $22M Oct 2025. So the 13 figures are lifetime totals across roughly 16 months of rounds, not money raised in 12 months; “seven rounds in 12 months” does not match the list.
What it means for scrapers. Heavily funded search and extraction APIs can price below cost for a while, which is good for you now and a lock-in risk later. The infrastructure layer is moving up-stack: Oxylabs now has outside capital and, by one competitor’s account, is preparing an agentic search API, so a proxy supplier may soon compete with the product you build on its IPs. Keep your fetch layer swappable.
Caveats. The tally comes from the CEO of a competing infrastructure provider, whose own claims (profitable, not competing with customers) are unverified. Browser Use’s $17M and an import.io $33M total added in replies were not confirmed against a primary source. Revenue rankings in the category are opinion, apart from Oxylabs’ self-reported ARR. Several rounds here were checked through headlines and snippets of the cited outlets rather than full reads, and valuations are as reported, not audited.
How it fits what the guide already says. The Akamai profile here describes
sensor.js as a roughly 512KB obfuscated script that runs its fingerprint suite inside the browser, collects 500+ signals across requests, and probes 60 extension URLs. A script inventory is the same idea one layer out. The extension probes ask what is installed in the browser. This asks what the page pulled in around it. Both are hard to fake from outside a browser because the answer comes from the real environment. It also matches the guide’s standing advice that scraping Akamai targets from raw HTTP works only where sensor scoring is light.What to do with it. If you emulate the sensor payload rather than run it, compare a real browser’s resource list for the target against what your pipeline presents. Aggressive ad or tracker blocking in a real browser could shift the set too, so test with blockers off as well as on. Running a real browser in the loop sidesteps the question entirely, which is the author’s recommendation. It is also the expensive answer, so price it before you adopt it.
Caveat, and it is large. This is one practitioner’s reading of one sensor build on one site, from a post with no links, no code and a screenshot we could not transcribe. We found no written version. We cannot tell from it whether the 47 domains are a hard check, a weak feature, or site-specific configuration, or whether other Akamai deployments do the same. Treat it as a lead to verify on your own target, not an established Akamai behaviour.
How it is shared. Anonymous browsing and API calls are capped at 100 rows per request. A free key, requested by email or GitHub issue with your name and purpose, unlocks full Parquet and CSV downloads and a read-only SQL console; only key hashes are stored and downloads are logged per key. Until v1.3.0 the tables sat in the public repo; from v2.0.0 they were moved to a private bucket, and the author says X’s developer terms on bulk redistribution are part of why. Code is MIT; tweet text is described as redistributed for research use, and anyone republishing is told to check X’s terms and prefer distributing IDs for rehydration.
The honest caveats. Posting full tweet text, even behind a key, is the author’s own reading of X’s terms, not a licence from X, and we have not seen X approve it. The README itself warns that raw IDs are unreliable for 12 of 52 source files (Modi’s file has 3,921 IDs across 56,941 tweets), so rehydration would fetch wrong tweets there. 21% of rows are replies, and one automated campaign day is 8.7% of the dataset. The repository was created on 20 Sep 2026 and had 6 stars at review; no peer-reviewed data paper is cited.
Why it belongs here. After X ended free academic API access, many researchers hold corpora they cannot legally re-share as-is. ESMD shows one defensible middle path: document the collection method, freeze and version releases, attach every bulk copy to a name, and route republishers to IDs. If you are building a research dataset from any platform, decide the sharing model before you collect, not after.
scrapy-stealth 1.0.0 (24 September 2026) lets the browser driver attach to an external CDP endpoint via STEALTH_CDP_URL. Read the source before you point it at a remote browser: in that mode the local CONNECT relay that injects your proxy credentials binds 0.0.0.0 with no client authentication, so anyone who can reach the port rides your paid proxy.utils/browser/relay.py, resolve_browser_relay_bind_host returns 0.0.0.0 whenever external CDP is on, and the advertised address is the machine’s outbound LAN IP, found by a UDP connect to 8.8.8.8. _start_browser_relay in _core.py then calls asyncio.start_server(handle, bind_host, 0). The handler never checks who connected. It reads a CONNECT line and adds Proxy-Authorization: Basic, built from your STEALTH_PROXIES user and password, before forwarding upstream. Under DNS-pin-only use it dials any host:port directly. The changelog describes the bind change as “auto-config” and says nothing about exposure.When it bites. This happens only with
STEALTH_CDP_URL set, and the changelog routes authenticated proxies and DNS pins through the relay. So the exposed case is exactly the one where a credential is attached. A locally launched Chrome still binds 127.0.0.1. On a cloud VM with a public IP and an open security group, “LAN” means the internet. Any host that can reach the port and send one CONNECT spends your residential bandwidth under your account. Its traffic, abusive or not, leaves from your exits.Two smaller side effects. The first is a disk write. Importing the browser engine runs
patch_nodriver() at module load, and it rewrites a stray 0xB1 byte in the installed nodriver/cdp/network.py inside site-packages, swallowing any error. The second is network traffic. Building the middleware starts a background thread that queries pypi.org with a scrapy-stealth/<version> update-check User-Agent, and no setting turns it off. Its “fingerprint rotation” is a weighted pick of curl_cffi and wreq profiles with header sets to match. In the browser it is only --window-size and --lang, with the stealth itself coming from nodriver.The caveat. This comes from reading source at the 1.0.0 tag, not from exploiting a live deployment. The relay port is ephemeral, so an attacker has to find it first. Nothing is published for pass rates, and the repo is three months old with 3 stars. The fix is to firewall inbound traffic or keep the browser local until the relay requires auth.
content-security-policy and x-frame-options so target pages load in hidden iframes.mellowtel-js repository (npm 1.7.0), the client opens a WebSocket to wss://ws.mellow.tel and receives crawl jobs. It then adds declarativeNetRequest session rules that remove x-frame-options, content-security-policy, cross-origin-embedder-policy and cross-origin-opener-policy from responses. That lets it load the target in a hidden iframe inside the user’s own browser. Before running, it checks a per-extension kill switch at mellowtel-bucket.s3.us-east-1.amazonaws.com/switch/<extensionId>.txt. SecureAnnex, reported by Risky Business, says the library waits for the user to go idle and linked it to Olostep, a scraping API.What the measurement found. The Northwestern AquaLab paper (Jang et al., IMC ’26) checked Chrome Web Store data and ran live tests. Only 16 of 116 Mellowtel-equipped extensions actually injected crawl iframes during a session. US nodes got up to 8.7× more crawl tasks than other regions. The work split into two patterns: shallow crawling of a long tail of domains, and repeated structured pulls from a few popular targets such as Google Search, CouponFollow and Reddit. The authors also found targets flagged for malware and a dynamic code-loading path that runs remotely fetched JavaScript and bypasses store review.
What it means for buyers and defenders. For buyers, removing the headers weakens the user’s own browser, and the user may be browsing an intranet on a work machine. A pool built this way carries that risk no matter what opt-in screen came first. For defenders, IP reputation and fingerprinting won’t flag this traffic because it really is a residential Chrome. What gives it away is behaviour: many sessions opened in iframes with no parent-page context, and the same structured pulls repeated across unrelated home IPs.
Caveat. Spur sells residential-proxy detection, and its own write-up could not be retrieved (a Vercel bot checkpoint). The figures here come from the IMC paper, SecureAnnex’s count and the SDK source. Mellowtel says users are opted out by default, must opt in explicitly and can revoke consent. It claims 10,000 active supporters, which fits the paper’s finding that the extension counts are an upper bound.
RemoteControl extension enabled by default that lets anything holding its token run Python inside a live crawl over localhost HTTP. The new official Scrapy MCP server is built on it: an agent can ask why the queue stalled or try a selector on a response already downloaded, without a restart. The same default is a new attack surface.RemoteControl extension, enabled by default, “which allows connecting to crawl processes via HTTP and running code inside them”. It needs asyncio (the default reactor), listens on a random localhost port, requires a bearer token, and writes the port and token to a job file under the user state directory (REMOTE_CONTROL_JOBS_DIR, default scrapy/job_files). Defaults: 30 s per call, 600 s ceiling, 64 KB of output. The scrapy-mcp-official server (Apache 2.0, Python 3.10+) reads those files and exposes four tools: list_jobs, status, execute(job_id, code) and inspection_reference, a map of scheduler queues, stats and engine objects so the agent knows where to look.Why it is genuinely useful. The telnet console always offered this, but only to someone who knew the internals. A stalled queue, a slot stuck on one domain or a selector failing on one layout is state that disappears on restart; inspecting it while it happens, and testing a fix against responses already in memory, removes the stop-edit-rerun loop that makes long crawls expensive to debug. The MCP server does not need installing into the crawl’s own environment.
Why it is also a risk. The project’s own README warns the server “is designed to run arbitrary Python code inside your crawl process”, with access to settings, environment variables and every secret the crawl holds. The trust boundary is same user, same host: any process running as that user can read the token file. That includes a coding agent that is itself reading scraped, attacker-controlled HTML, which is exactly the indirect prompt-injection path this guide warns about. On shared runners, CI boxes and containers that mount the home directory, set
REMOTE_CONTROL_ENABLED = False unless you mean to attach.What is not established. This is a vendor announcing its own feature, with no measurement: no numbers on time saved, no audit of the token or endpoint, no statement on whether agent-run
execute calls are logged. The risk reading above is this guide’s inference from the documented defaults, not a demonstrated exploit, and the telnet console carried a similar same-host exposure for years.200 OK: the body was a Cloudflare “Just a moment…” interstitial, and the pipeline stored it as data. The fix is a pure detector over {status, headers, html} that checks for a challenge before trusting any status, since the same interstitial arrives as 200 or 403.200 OK; the body was a Turnstile interstitial titled “Just a moment…”. Because the write gate was the status code, the challenge HTML flowed downstream as if it were a product page. The guide already warns that a 200 can hide failure; what this adds is a concrete, copyable signal list for the Cloudflare case and an ordering rule.The signals. HTML markers, matched case-insensitively:
challenges.cloudflare.com/turnstile, a cf-turnstile container, the __cf_chl_ and cf_chl_opt namespaces, a Just a moment title and Checking your browser text. Header: cf-mitigated: challenge is decisive on its own; server: cloudflare and a cf-ray only tell you who fronts the site, not that you were challenged, so never classify on them alone. The detector is a pure function with no network calls, which is the point: you can replay it against saved responses.The ordering and the verbs. Three outcomes, checked in strict order:
challenge (any marker, whatever the status), then ok (no marker and a 200), then hard_block (error status or other block signal). Challenge runs first because the interstitial comes back as either 200 or 403, so a status-first branch misfiles it both ways. Each outcome gets a different action: only challenge is rerouted to a rendering path, hard_block means back off the host, and the rerouted response is run through the same detector again before it is written. Keep saved challenge pages as regression fixtures, and chart the reroute rate: a jump is your earliest sign the target changed its protection.The caveat. This is a vendor postmortem, and the reroute step is Crawlbase’s own paid JavaScript token; its 39.1% valid on generic US proxies versus 99.5% via its API is self-reported with no method published, and the 8,000 solves per second figure is a concurrency model, not a benchmark. The marker list is the part to take, and it is only as current as Cloudflare’s markup, so verify it against a fresh challenge page before relying on it.
Oct 2026 update. A marker list misses gates with no challenge vocabulary. One
httpx request to Amazon’s homepage (2 October 2026, n=1) got a 200 and a 3,781-byte page saying only “Click the button below to continue shopping”, with no CAPTCHA or robot wording to match. Counting one expected element caught it. A day earlier the same block came back as a 202 on nine Amazon stores and an empty 200 on three, so run the content check on every 2xx, not only on 200.puppeteer-extra-plugin-stealth 2.11.2 was published on 1 March 2023, the repo’s last push was July 2024, and npm still counts about 835,000 downloads a week. Browserless’s new BAP SDK is the vendor answer: stealth as a server-side endpoint, not a package.2.11.2 as latest, published 2023-03-01; the GitHub repo berstend/puppeteer-extra is not archived but was last pushed on 2024-07-18 with about 275 open issues. npm’s downloads API reports 834,764 installs for 15–21 September 2026. Chrome has moved about 44 major versions since. Every evasion in the package is a JavaScript override of a property as Chrome 110 exposed it, and the guide already covers why that is a liability: a patched getter shows up under Function.prototype.toString() inspection, and new detection surfaces appear with each release that nobody is patching.What BAP does differently, per its docs. BAP does not speak CDP. Each
Page method builds one BrowserQL (GraphQL) mutation and sends it as a single JSON frame over a WebSocket, queued serially; that is why its method set is a subset of Puppeteer’s. Stealth is a route change, /chromium/bql to /stealth/bql, where the fingerprint work lives in the browser build and adds “entropy injection” per request. type() defaults to a random 50–200 ms per keystroke, and both type() and click() scroll into view first. emulationOs swaps UA, Client Hints, platform, GPU, cores, fonts and audio latency together, but timezone and locale follow the proxy, so you must match them.The practical upgrade path. If a Puppeteer fleet still imports the plugin, treat it as unmaintained: pin, audit, and test the target with it removed, because an override can be the tell. Your options are a maintained patched-driver stack, a patched browser binary, or a hosted stealth endpoint like this one. The hosted route moves maintenance to the vendor but also moves the browser, the proxy and your traffic onto their bill.
The caveat. This is a vendor launch by the vendor’s founder, and neither the post nor the docs nor the launch blog publishes a single pass rate, detector score or benchmark; “what it patches” is not listed. BAP is not a drop-in replacement, lacks full Puppeteer parity, and the Python SDK is “coming soon”. Test
/stealth/bql against your own targets before believing it.Oct 2026 update. Browserless’s OS-emulation page now says
emulationOs takes four lowercase values, silently ignores anything else (macOS included), and defaults to Linux; its changelog moved the cloud default to Windows, then back. The desktop list omits fonts. The founder’s demo used bot.sannysoft.com, a weak page, and its GPU (Intel) differs from the run log (NVIDIA). A commenter suspects toString() exposes it; untested.The number that disagrees with this guide’s other bot shares. The same report says humans generated about 73.4% of requests on DataDome’s customer sites, bots and agents about 26.5%. Set that next to Cloudflare’s 57 to 58% bot share of HTML requests and Imperva’s 53%, and the lesson is the denominator: a population of sites that already runs bot protection, measured after classification, is a different thing from HTML requests across a CDN. None of these is “the share of the web that is bots”.
The growth figures, read as attempts. Bad-bot traffic +124% against human +13.2% (the “9x faster” headline); scraping is 70.9% of bad-bot traffic and up 185.2%; AI traffic +82.3% to 52.7 billion requests; in H1 2026 AI agents sent 605.6 million requests to logins, forms, carts, payment and sign-up pages, 51.7% of them to logins. The report itself says traffic classified as an attack is an attempted action, not a successful attack.
The caveat, stated plainly. This is a bot-protection vendor grading the market it sells into: “65% unprotected” is also a sales pitch, the ten bot profiles and the pass/fail rule are not published in detail, the test only touched homepages (many sites protect login and checkout, not the front page), and “scraping” is whatever DataDome’s classifier labels scraping. The primary report page refused automated fetches, so figures here come from DataDome’s press release and consistent trade-press summaries. Treat the protection test as a directional base rate, not a census.
agent_helpers.py mid-task. The launch post sold it as about 600 lines; three core files in the repo now total 1,975. Its own skill file says: plain public page, use curl.browser-use/browser-harness (created 17 April 2026, about 18,000 stars when read in September 2026) attaches a local daemon to your running Chrome over the DevTools Protocol after you tick the box at chrome://inspect/#remote-debugging. The agent sends Python via a heredoc to a browser-harness CLI with helpers pre-imported, and raw CDP is always reachable as cdp("Domain.method", ...). The core under src/browser_harness/ is protected; the agent writes new helpers into agent-workspace/agent_helpers.py, which ships at 7 lines. The launch essay, The Bitter Lesson of Agent Harnesses, argues that every wrapper like click() is a decision imposed on a model that already knows CDP from training data.The worked examples. Per that essay, the agent hit a missing file-upload tool, wrote an
upload_file() helper on raw CDP, then switched to a chunked strategy after hitting a 10 MB websocket payload limit, and handled cross-origin iframes and shadow DOM without an abstraction layer. These are vendor anecdotes with no run count or success rate. A companion post describes the cloud version: a second agent reviews each finished trajectory, extracts a “skill” (URL pattern, recipe, steps to skip) behind an LLM PII gate, and reports one Duo 2FA button id reused by 254 later agents.Lines, measured. The essay cites
run.py 13 lines, helpers.py 192, daemon.py 220. Fetched from main in September 2026 they are 413, 668 and 894, beside new modules for auth, MCP, recording and macOS approval, plus about 97 community domain-skills/ folders (Amazon, Booking.com, arXiv and others), off by default behind BH_DOMAIN_SKILLS=1. The core grew about 4× in five months, so the core does keep growing even though agents also write their own helpers.The caveat, and the scraping rule. This tool is aimed at logged-in, personal browsing. One daemon holds one mutable current tab, and the skill file warns that two agents switching tabs at once can race and screenshot each other’s page. It routes captcha-prone scraping to its paid cloud, a first-party upsell. And its own
SKILL.md says a public page “needs no browser”: try curl first, and only if that fails or returns a shell page, move up to the browser.The arithmetic, done here. Token cost per build is $5 + $2.50 = $7.50 (easy), $22.50 (medium) and $112.50 (hard). Per request, an HTML page over residential proxy costs about $0.0004, and a rendered page through a scraping browser about $0.0136, 34× more. At 100,000 hard requests, the article’s threshold for building to beat buying, the bandwidth alone is about $1,360, twelve times the token bill. That is why the article concludes: “The cheaper the code got, the less it helped on the targets where the code was never the expensive part.”
The split it recommends. Build the code layer, which now costs close to nothing and belongs to you. Buy the infrastructure layer (proxies, unblocking, browsers) unless scraping is your core business. Run execution on capacity you already have. It also notes a “5% nobody sells”: targets such as Chinese e-commerce sites with custom obfuscation, which vendors do not find worth serving, so you still need in-house R&D. Building is “cheap to start now and expensive to leave.”
The caveat. The piece is partly paywalled. The readable part stops after “No model does that part for you yet,” before the per-tier break-even request counts, so only the percentage drops are confirmed. The per-request figures above are our own multiplication of the published inputs. The article’s own “near-zero” code cost assumes a team with an established LLM workflow, and it does not count the monitoring staff a built fleet needs.
scrapy-jev ships three parts: a JevClient that reads fields through itemadapter and returns a score per field, a QualityGatePipeline that scores a fixed sample, and a self-registering addon. Every field on an item must clear its threshold for the item to pass — deliberately not a majority, because one bad field should fail the item — and the spider closes once the sample’s pass rate falls below the configured level. Defaults: sample 20 items, pass rate 0.7, per-field 0.5, pipeline priority 400, model typesafe/jev-latest through OpenRouter’s alpha decisions endpoint.The numbers, all from the author’s own demo. A three-field scoring request ran 570 input tokens and cost $0.000024, against $0.042 per million input tokens with output free; a four-field item across a million-item crawl works out at roughly $30. Responses land in 70 to 500 ms. Breaking the SKU selector so it returned the title scored that field 0.08 to 0.35 while the other fields stayed above 0.9; blanking the name field failed every item in the sample and stopped the crawl. Renaming a price class correctly did not fire, because the regex extraction still worked — the gate is watching the value, not the selector.
The framework trap is worth the read on its own. Raising
CloseSpider from inside a pipeline does nothing: Scrapy wraps process_item in a bare except Exception, so the close is swallowed like any other error and the crawl carries on. The working version calls crawler.engine.close_spider_async directly. That is Scrapy behaviour rather than anything to do with this model, and it will bite any quality gate written as a pipeline.What it cannot do, in his own words. It only catches breakage that produces an implausible-looking value — “a selector that starts pointing at the wrong product on the page, but returns something that’s still shaped like a real name” passes cleanly. It is sample-based, so a crawl can run through bad items outside the sampled window. It adds a hard dependency on an external API that can be slow, rate-limited or unavailable. And the thresholds are “guesses… tuned against one demo site” — a fictional 1,000-product coffee shop he built — with, he says, no evidence yet that a 0.7 pass rate is right for production. Read it as a vendor-employed engineer publishing a working pattern and its defaults honestly, not as a validated production configuration.
f6ad334abefc, captured 15 September 2026. The interpreter separates from its encrypted program at 62,751 characters, with the encrypted p field at 16,831; lifted to readable form it takes 39 MBA rewrites and 4,258 formatted lines. The program image is protected by an ARX block cipher at 15 rounds, round constant 1819, rotation 3. The dispatch table holds 98 handlers with 93 operand layouts, sorted into control-flow, alphabet, string and re-key families. A 2,000-call dispatcher trace (1,780 outer fetches, 220 queued dispatches) ran 1,220 instruction starts covering 52.1% of the image bits with 127 emit operations, yielding a 635-byte plaintext record before SG_SS encryption.The moving-target detail is the durable lesson. Google changed handlers, opcode assignments, cipher parameters and execution flow while the research was running, forcing rebuilt profiles, repeated traces and revalidation against fresh challenge pages. So a recovered opcode table is a photograph of one build, not a capability you own — the same maintenance tail this guide flags for every pinned artefact, and the reason the cheap top-layer map keeps earning its place.
The publishing model is worth copying. The tooling is MIT on GitHub and the licence explicitly does not extend to captured Google code or to artefacts derived from a supplied response; analysis runs offline against a capture you make yourself. That is the same ethical shape the earlier card praised, and better than shipping someone else’s obfuscated bundle in a repository. The derivation, the diagrams and the failed hypotheses sit behind a paid subscription, and the README is explicit that the tools stay usable without it.
The caveat. Four stars, no forks, no issues, no pull requests — nobody has reviewed this. Every figure above comes from the repository’s own README describing one author’s run against one capture of one build, not from independent replication, and the reasoning that would let you judge the method is the part you cannot read. Useful as a map of what the bottom layer costs, not as a verified result — and the earlier card’s point stands untouched: if all you need is which properties the challenge reads, none of this is required.
Why this sits beside the coherence finding rather than replacing it. The anti-mage result — every anti-detect build scoring “modified” across 23 cross-surface checks while stock browsers scored a flat zero — is about whether a browser contradicts itself. This is about the prior applied before that reading is weighed. Both can hold at once, and if they do, a coherent profile in a poorly-trusted segment loses to an incoherent one in a well-trusted segment. For a defender the consequence is sharper than for a scraper: a consistency check alone is a weak gate, because the cheap response is not to fix your signals but to migrate to a better-trusted segment.
The operational lesson survives even if the numbers do not. His own summary is that every wrong conclusion he reached came from inferring rather than instrumenting, which is the same discipline the bracketed-control slider run demonstrates elsewhere in this guide: hold one variable, keep a known-good control open and close the run, and believe the brackets rather than the theory.
The evidence is the weak part, and it has to be said out loud. There is no link, no published methodology, no raw data, no sample size and no named target. “100%” and “~20%” are quoted without denominators or run counts, the post carries a single reaction, and the author is describing his own unpublished research while openly looking for work in the field — every incentive points at a striking framing. Treat it as a hypothesis worth instrumenting on your own targets, not a result that overturns anything. The cheapest test is the one already in this guide: bracket it.
curl gets a Cloudflare 403; a perfect Chrome User-Agent gets a byte-identical 4,545-byte 403 — keyed on the handshake, so no header value moves it. wreq 0.16 walks home → category → product 200/200/200 with zero cookies out and datadome absent on every hop, and --cold, an empty jar per hop, still serves all three.Same URL, 403 or 200, by how you asked.
bck.hermes.com/products as a top-level navigation (sec-fetch-mode: navigate) returns a 1,130-byte DataDome block page; issued as the page's own JS does (Origin, Referer, sec-fetch-mode: cors, sec-fetch-site: same-site), the identical URL with the same empty jar returns 200 OK at 35,042 bytes, no token anywhere. Scope it to one endpoint on one site: DataDome rules are per customer and path. Inverts the lean-headers card: there fewer headers, here the right kind.Where it bounds the existing card. Cold fan-out over the per-product endpoint logs 1 of 8 extracted, 7 blocked; a freshly minted 21-cookie session gave, in his words, 2 successes instead of 1. One ~80-second mint bought one to five products — his average, not a canonical cost. On a 12-product queue one browser took 12/12 in 58 s, mint-and-rotate over three exits 7/12 for 244 s of browser — 7× dearer on that one run. Two variables move: a clean residential line versus commercial exits the post reports are refused on product URLs. The penalty is not per-request: the product page then 403d from that address, reportedly still blocked 14 hours later — one observation, no measured TTL.
The caveat is large. n = 1 — one target, one connection, runs of 8 and 12 products, self-reported; only the repo's public MIT status was independently checkable. Nor is it a clean A/B: the card it corrects warmed that endpoint with Hyper Solutions' paid solves, so whether it is intrinsically metered or merely punishes under-warmed sessions is unresolved, not refuted — and the 7× counts browser-seconds, excluding the paid-solve line that write-up was optimising.
fingerprint-scan.com (September 2026) scores the same timings. Rotating the pool moves neither, and neither publishes an accuracy or false-positive rate.Sakura-sx/Aroma (Fastly VCL, 122 stars, 21 December 2025, no licence file) scores a visitor with client.socket.tcpi_min_rtt / client.socket.tcpi_rtt — the kernel’s min and smoothed RTT from tcp_info. Bands: 1.0–0.7 normal; 0.7–0.3 normal on an unstable link; 0.3–0.1 possibly a proxy; below 0.1 flagged; the VCL blocks only under 0.1, a cut its author set deliberately low. No IP intelligence at all, so it misses most VPNs; scoring and blocking sit inside if (client.socket.tcp_info), so an HTTP/3 visitor is never scored — the shelter side of the HTTP/3 card. Its README: “not ready for production.”The only measured separation in this family is five years old. Nikolai Tschacher, June 2021, n=23: a browser WebSocket RTT against the server’s SYN+ACK-to-ACK handshake delta. 11 visits he could only assume were clean (“I cannot say for sure”) agreed to within 0.7% to 13.8%; 12 samples through exactly two providers, Bright Data and ScrapingBee, ran “45% to 136% larger”. Both clocks sit at the server: through a
CONNECT tunnel the transport clock times only the proxy→server hop, the application clock also the hidden client→proxy leg. Aroma is not that comparison (no websockets, its author says), and 2021 measures neither newer detector.What none of this shows. Neither detector has a published accuracy or false-positive rate. Vastel’s post says only that the row uses “network timing features”, though he “mostly trained/tested it using residential proxies, so it may underperform on data center or ISP proxies”; the richer account (CatBoost, ~27 latency-and-ratio features) rests on one comment we could not independently surface — reported, not confirmed. Aroma’s field numbers are single-operator HN self-reports: a Ping Proxies founding engineer catching “a resi-proxy about 50% of the time” against his own company’s pool, benmmurphy about 5% against his own
CONNECT proxy, “maybe only that often due to a bug”. Instruments, not verdicts.src/providers/scrapingbee.ts landed 6–7 September 2026, and maintainer commit edba7b7c names the cause: it had fetched the targets “from the default datacenter pool with neither premium_proxy nor stealth_proxy”, finishing last at 29.7%. So the mechanism is the proxy tier, not the rendering switch: render_js: true on that pool in July and August, mode: "auto", max_cost: 75 in September. Four providers ran byte-identical adapters in both runs (Decodo largest at −17.1); only their movement is chargeable to a provider.The 32–0 against JavaScript rendering is column sizes — and the maintainers said so first. Among six caller-selected providers the README reports 32 of 100 targets passed by an unrendered provider alone against none by a rendering one, 37–0 at a majority and 45–2 at all five; all three reproduce. But that arm holds four providers (Scrapfly 86.2, ScraperAPI 84.0, Scrapingdog 45.6, ScrapingAnt 36.4) against two rendering (Decodo 50.6, ZenRows 41.2), and commit
15b8bc19 (6 September 2026) had already called that revision “an artifact of how the two columns were built”. Strength-matched, our recomputation gives 29–14, 25–26 and 15–29; no provider ran both ways on one target, so flag and provider stay inseparable.The latency column charges a provider’s failures to the providers that succeeded: no verified content on a target earns the nearest-rank p75 of successful providers’ scores. So ScrapingAnt’s 18.95s is 47 own fetches against 52 borrowed, and 36.4% success ranks faster than Scrapfly’s 86.2%.
Caveats, and whose numbers these are. Vendor-owned repo:
string tops all three runs (95.8%, then 97.0% twice), though the README volunteers that it routes server-side and is “not parameter-comparable with a pinned-tier provider”. The 90-target comparison, the matched arms and the borrowed-latency split are ours; the +40.7 attribution is the maintainers’ diagnosis plus dated commits, not a controlled A/B.RETAIL-INVESTIGATION.md in lightpanda-io/agent-benchmarks, dated 4–19 July 2026, one target: allbirds.com, a “Shopify, Vue-based theme”, collection page plus three product pages. Their diagnosis: “HTMLScriptElement.supports and DOMTokenList.supports (relList) don’t exist, so the theme’s loader falls back to fetch()-based chunk loading.” Nothing errored — feature detection is built for a negative answer — so the loader took its legacy path and fetched vue.esm-bundler.js nine times per page against Chrome’s once. HAR over two pages: stock 676 requests, Chrome 432, patched 305. One theme, one site, the engine’s own authors.The fix is stricter than
return true — and it was the smaller lever. Merged lightpanda-io/browser#2886 answers true only for classic/module/importmap and validates rel tokens against their defined set: a probe answering true unconditionally trades a latency bug for a coherence tell. The patch alone moved the retail median 5.98 s to 5.22 s, 1.27× Chrome to 1.17×, still behind; parity came from a default instead: lightpanda installs no HTTP cache unless --http-cache-dir is set, and that flag alone gave 4.82 s (−19%), which the document calls statistical parity with Chrome’s 4.70 s on overlapping IQRs; both together 4.60 s at 1.03×. The ~46% and the 1.27× anchor on different Chrome baselines.The caveat, and what survives the fix. Every figure is the vendor measuring itself: reported medians, raw per-run data not recomputed, no independent replication found — absence of evidence, not refutation. Loss and wins are two months and two configs apart: July, stock,
allbirds.com, against a 15 September 2026 launch post on another target, after #2886 merged and with the cache on. Both causes of the ~46% are closed in the published config. The lesson outlives the patch: engine gaps arrive as latency and request volume, not errors. Before trading a heavyweight browser for a light one, diff request counts against a real Chrome, and confirm a cache directory is set.LICENSE at g1879/DrissionPage is a bespoke dual-language grant permitting use “in their personal capacity, but only for the purpose of learning and legal non-profit activities”, and an organisation “may not use the project’s source code or binary form for commercial purposes without authorization”. Then the clause the post skipped: “Authorization will automatically expire if any of the terms are violated during use” — and the fourth term is “Follow the Robots protocol and do not… collect data that is prohibited by law or the system’s Robots protocol.” The same terms ship in LICENSE, the README’s terms-of-use section and the installed package’s own __init__.py docstring — verified in the 4.1.1.4 and 5.0.0b1 wheels. The commercial-use clause stops a commercial estate outright; auto-expiry bites whoever has authorisation.Why an identifier-keyed licence check sees nothing. The GitHub API returns
spdx_id: NOASSERTION; PyPI’s 4.1.1.4 record has license and license_expression both null and no License :: classifier. The identifier is missing, not the text — the wheel METADATA carries License-File: LICENSE. An SPDX-keyed gate sees a blank; a scanner reading License-File or the package’s docstring surfaces the condition. Grep your lockfiles for drissionpage before it reaches a commercial pipeline.What none of this establishes. Not legal advice: “commercial purposes” is undefined, automatic expiry on breach is a unilateral term of untested enforceability, and we found no record of it being enforced. It does not make robots.txt binding — see the legal section; what is new is a library making it a grant condition. Two claims stay attributed: “runs faster” than Selenium is the author g1879’s own claim about his own project, with no published benchmark, and the post’s “ecosystem of MCP servers and CLIs for LLM agents” is reported by the post, not confirmed by us. The README also carries five residential-proxy affiliate placements — odd beside a non-profit-only licence.
The one hands-on test, scoped. Antonello Zanini, The Web Scraping Club, 15 Sep 2026: the CLI got clean Markdown from L’Occitane, Temu and Nike, then failed on Cloudflare-protected Hermès and Canada Goose — as did the paid cloud tier, on the same pages: “the Cloud API wasn’t able to scrape any pages that the CLI couldn’t already scrape… may not be working as expected.” Scope it: anti-bot coverage, not the paid tier, which did deliver on plain JS rendering where the CLI did not. The vendor claims only that the API “attempts” protected-page handling.
What is real, and two fixes. Rust CLI plus hosted API: AGPL-3.0, v0.6.22, Firecrawl-compatible
/v2 — Firecrawl code repoints without a rewrite. primp TLS impersonation of Chrome 142 appears only in the March post — reported as-of-launch, not confirmed current. Post-only numbers: extraction core 0.8/3.2/12.1ms at 10/100/500 KB excluding network, one article at 4,820 → 1,590 tokens. Two errors: the #ai link and library row 404 (live github.com/0xMassi/webclaw), and pip install webclaw is printed for a Rust binary with no PyPI package (npx/brew/cargo/Docker).The caveat. The benchmark is the vendor grading himself, no scoring definition or page list: unverifiable. The review is one person, one session, one target set, publishes no pass rate, hedges “At least, in my tests…” and discloses a sponsored 1,000,000-credit plan — in his favour. The post names no limitation at all — no JavaScript challenges, CAPTCHAs, Turnstile — silence, not candour; the overclaim lives in its headline, “bypasses Cloudflare without a browser.”
google_serp): 20.0% (33/165) cold against 84.4% (152/180) at depth 7, z = 12.0, recomputable from rows committed in data/runs/ (filenames as the README gives them, unconfirmed). Drift: its depth-2 rung now reads 24% over 34 against our card’s 19% over 48.The held phase is what makes warming affordable.
NOTEBOOK.md: 108 of 109 held rows passed, by position 36/37, 36/36, 36/36. The README’s separate held.py cut gives 96%, 98% and 99% at positions 2, 3 and 4, no denominators printed. The bill — “at --dwell 20,45 the three warm rungs average 65, 130 and 195 seconds per identity” — leaves the post’s “four minutes” reported-plausible, not measured, and no per-session total is published. Every NOTEBOOK.md figure here is a partial read: it fetched truncated.The caveat, and the post-only numbers. The README discloses “built and maintained by NodeMaven, who sell proxies” and the post is their CEO, so “residential sessions work well once warmed” is a vendor’s claim about its own product. Three of its figures — 71% of refusals arriving as HTTP 200, a refusal 76× smaller than a real answer, 10 MB per session — are in none of the repo’s three write-ups: absent from what we could read, not from the data (no
.jsonl row was opened). The fourth, 609 of 612 follow-ups, is an unreconciled divergence, not a refutation: three undenominated positions at ~204 rows each would be 612. Its cited PDF is unreachable outside LinkedIn; every check ran against the repo.Still unmeasured: “what the depth is doing is not in these rows.” The owed run: clear the jar at the deep rung, same exit and profile, rule pre-committed — near 82% the state is target-side, keyed to the exit; near 11% the browser’s own jar carried it.
Oct 2026 update. NodeMaven, which sells proxies, shipped the ladder as an MIT tool,
google-browser-scraper (created 1 October, 27 stars on 12 October). Its launch figures, 33/165 cold against 152/180 warm, are this card’s pooled runs, not a test of the tool, which dwells 3–8 seconds per warm-up page against the measured 20–45. A fourth run now reads 19% (13/67) against 88% (257/291), z = 11.8.Runtime.enable as an automation tell in six places, always as a timing or handshake artifact — never the mechanism behind the name: a getter on an Error’s stack that fired when an attached inspector previewed the object. Two V8 commits closed that getter in May 2025, and the shipped branches settle which milestone carries the fix and which read is still live.stack getter trick has been dead since stable Chrome 138 — but name is never an own property of an error, so the error-description helper still resolves it down the prototype chainvalue-mirror.cc under bug 415094795: 61a907540d (2025-05-06) and e08e973474 (2025-05-07, committed 2025-05-09), both by Pavel Feldman. That bug is behind a Google sign-in wall, so its title and status come from commit footers.Which Chrome: 138, not 137. Only crawlex names a milestone, the M137 train, and it is wrong. Chromium Dash: M137 branches 2025-04-28, stable 2025-05-27; M138 branches 2025-05-26, stable 2025-06-24 on V8 13.8 — both commits land in that window; the code agrees:
13.7-lkgr’s value-mirror.cc (75,978 bytes, byte-identical to tag 13.7.152.14) has no getErrorProperty; 13.8-lkgr has the guard; no merge-back. Caveat: those proxy the shipped M137 branch: branch-heads/13.7 returns HTTP 403 on googlesource file logs.What survived is the inherited
name read. getErrorProperty() takes GetOwnPropertyDescriptor and returns empty when the own getter is script-backed; a prototype getter does not evade it, per the expectation file from e08e973474. But stack is an own accessor, while name is spec-inherited and never own — its empty descriptor falls through to object->Get(context,"name") and the page’s Error.prototype accessor. Read out of shipped source, not run against a Chrome build.Who says so, and what nobody measured. The obituary is Castle’s — anti-bot vendor, own blog, no measurements, no version — and “only basic bots” is what a vendor says about a method it just lost. Scrapfly reports one run on one Chrome 152 build: Brotector saw
stackLookupCount:0 but nameLookupCount:3 under Runtime.enable — not reproduced here, and not a rate. dev.to/shuicici (unreviewed) named getErrorProperty first; the branch check is ours. Vendor prose came via a summarising pipeline; commit metadata, diffs and source are byte-exact. A human with DevTools open trips the same path, so the old stack tell scores noise; the startup handshake shape is untouched./goto teardown card says flatly that manually driven Chrome gets it too, so automation is not the trigger. A clean control breaks that: one fresh, hand-driven Chrome profile on one query returned 213 direct links and zero wrappers. The debug-port arm everyone repeated agrees with our existing source — the control is the half that moves our text.--remote-debugging-port: 96 / 0 / 106, then 83 / 0 / 93. Two flag trials against one control, one query, one account state — not a controlled study.It corrects our sentence, not the other study. ScrapingBee’s teardown also used a debug port and also got wrapped links (“remote debugging was not required to trigger the new link format”), so both agree there. The divergence is the baseline: their hand-typed profile did get it, this one did not, so our line becomes “not the only trigger.” An unverified reader report of
/goto in hand-driven incognito and Firefox fits neither model: the trigger is unsettled.No mechanism, and the flag may have been inert. An unattached debug port is a
localhost listener no remote server can observe, and the CDP tells never fire; no explanation is offered, so the flip is observed and the channel unidentified. Our Chrome 136 card notes the flag needs a non-default --user-data-dir, and the post names neither a Chrome version nor a profile directory — so either the arms differ twice over or the port never opened.The caveat, and one anomaly the post passes over. Total links nearly halved — 215 against 106 and 93 — so something beyond link format differed between arms; the author’s stability check holds within an arm only. A week later he downgrades himself: signed-in state outweighs his two windows, and “the classifier looks at more than one signal.” It spread via a LinkedIn post by Stan Sadokov, CEO of proxy provider Nodemaven, whose cookie-farming prescription is in no primary source, labelled “not a proven fix” there; the vendor replies under it are self-reports.
The cheap check. A 12-request resolver probe found no client and no
Referer effect, so the redirect costs what our open-questions card prices. Count wrapped versus direct under your own launch config before budgeting an extra request per result./goto cards price resolution at one GET per unique token, and the token is still AEAD-style ciphertext, undecodable locally. Three September sources add that Google still ships the destination in the same response for most result types, in the render-state blob its own JavaScript reads. Where that holds, the per-token GET is the fallback, not the common case.href and left the page's state object alone. Google published no explanation; treat the mechanism as observed, not motivated.Coverage, and the real residue. On that single capture the blob paired 7 of 8 web results, 9 of 9 forum results and 6 of 7 organic results on an AI Overview page; the one web miss is a YouTube video whose URL sits in the inline player's
data-curl/data-surl attributes — so on that sample all 8 web results came out of the response already paid for, no /goto call. Named residue: Lens image matches, some AI Overview video sources. Scrape.do, describing its own product, ships the two-tier shape: link from the page “at no extra cost”, resolveGoto (default false, no extra credits) for the rest. Aberastegue's Google-Goto-URL-Extractor, with nothing to sell, never calls /goto and pairs by “smallest common subtree—the deepest shared ancestor”, not fixed array indexes — the part that survives a layout change.The arithmetic, and the caveat. Nozzle's 500–1,000 requests for one five-page ranking and the ~110 req/s from one home IP stay true as the fallback budget: parse what you already paid for, resolve only the residue. The stake is silence, not the bill: GrowByData, self-reporting from its own product with no methodology published, saw YouTube's share of voice fall from 13–15% to near zero over six broken days to 1 September 2026. But it is one capture, one date, denominators of seven to nine — single-sample counts, not a benchmark. Both corroborating sources sell SERP APIs and published no raw data; the independent one publishes no coverage figure at all; nobody has published a plain-
curl confirmation either way, and ScrapingBee's and byteiota's denials never examined page state — silence, not contradiction. Prove it on your fetch path first.Sep 2026 update. A practitioner write-up adds Google Images to the list: destination URLs there still appear inside the page’s JavaScript data, so image results start as a parsing job too. For the residue it gives the same method this guide already uses: GET with redirects disabled (
allow_redirects=False) and read the 302 Location header. It does not contradict the HEAD finding. The Images claim is one unmeasured report, and we could not read the full article.Prevalence, not detection. It counts any script, so 70 pages reading anti-debug APIs is not 70 sites running a defence — the vendor says as much: a tall bar is a popular signal, not proof of detection. The 21 labels are never defined,
unresolved receiver least of all, so the seven 100% rows are six plus a question mark. canvas+webgl, the scraper-forum obsession, is 56/71, behind storage (69) and workers (67). And 23/71 is an upper bound on the denominator The WebRTC Trap and The UDP Leak lack: touching an API is not comparing an ICE candidate against the proxy exit, and zoom.us is in the tables using RTCPeerConnection as a feature. No case against Camoufox’s geoip=True, which also aligns timezone, locale and Accept-Language; navigator and timing are 71/71.Whose number it is. Richards Software LLC’s Automation Benchmark is beta-stage, this is its blog’s only post, and this guide recommends it — which raises the bar on its self-published figures, not lowers it. No crawl methodology is published: browser build, trapping, timeout, headless versus headful and exit geography unstated. The 29 walled or unfinished sites are unnamed, so reading them as the toughest defenders is our inference. And 84.5% automation-artifact reach is one snapshot of 71 unusually instrumented pages: the nearest independent measurement, Gundelach et al., arXiv 2606.14525 (June 2026) over the first 10,000 Tranco sites, found 3,649 of 7,944 scanned sites (46%) probing a Tier 1 automation signal and
navigator.webdriver read on 2,730 (34%). Neither replicated.0xlearner put five cells through a live DataDome slider on idealista in 62 seconds — a known-good control opening and closing the run, one variable changed inside each pair. The only two cells blocked were the two whose user-agent contradicted the rest of the browser. Both controls wore a Windows user-agent on a Linux host and solved, so the gate is coherence, not honesty.control-open_fortress (Fortress Chrome/150, Windows UA) SOLVED → local-touch (stock Chrome/153.0.8010.5, X11; Linux x86_64) SOLVED → local-touch-win (same binary, Windows UA forced via --user-agent) BLOCKED → fortress-linux (Chrome/150.0.0.0, bare (Linux) token) BLOCKED → control-close_fortress SOLVED. Both failures are incoherence, not weakness: a Windows UA over a Linux navigator.platform, and Mozilla/5.0 (Linux) is a token desktop Chrome never emits.The post’s headline flag is reported by the author, not measured inside this run.
--blink-settings=maxTouchPoints=10,availablePointerTypes=6,… (printed truncated) is credited with flipping a hard-blocked local Chrome to SOLVED, but that leg sits outside the brackets: ~26 minutes earlier, stealth layer off, logged “no slider served” — the signature of a gate earlier than device coherence.Anti-bot A/B testing is stateful, so the control is not optional. The post reports “six failing attempts in twenty minutes” and that a burst degrades the egress IP for ~15 minutes, after which every cell fails identically to a genuine device-signal block — a false negative with no signature. Verbatim figures, but one narrated observation from one home connection. The protocol is cheap: control, tests, control again; discard unless both pass. Nothing announces this window and testing causes it, so unbracketed browser-versus-browser tables may be scoring IP decay.
The caveat is large. Every cell is n = 1 inside a 62-second window, two sites, one home connection — a verdict flipping with the user-agent, not a success rate. The baseline is Fortress (tiliondev, BSD-3-Clause), which its own README calls a “stealth Chromium engine” — a vendor claim about its own project — and which lists DataDome under Roadmap. The behavioural layer is near-untested: one replayed trace (55 points, 242 px, 1,246 ms) was never refused, which is not the same as undetectable. Bylines differ (Tauqeer Abbas on LinkedIn,
0xlearner on the article), unconfirmed as one person.0xlearner, counted it over nine mints: 21, 48, 61, 67, 70, 94, 98, 106, 106. A 5× spread no constant serves — and his blocks were tracking his own stealth layer.The blocks tracked his stealth layer, not CDP. Vanilla
google-chrome on a fresh --user-data-dir loaded the category page with no challenge, on the same binary and IP being blocked under CDP. Bisecting one layer at a time found his evasion script redefining window.outerHeight as innerHeight + 85. Magnitude is not the tell — real headed Chrome without a bookmarks bar shows roughly 85–90 px there. Invariance is: hardcoded, the delta never moves on resize or with a bookmarks bar, where a real one’s does. His fix disables stealth on headed runs. Treat it as reported: one vanilla load, a sequential observation, not a paired A/B, in a week the target changed behaviour twice; it does not show CDP is safe at volume.What this does not show. n = 9 sessions, one target, one DataDome deployment, one operator, self-published, no raw logs or harness; DataDome is tuned per customer and path, so 21–106 may belong to this deployment. That the budget is counted in requests rather than elapsed time is his reading, not a tested result, and his run log shows 578 requests at 18.3% success that we could not place as pre- or post-fix. He does name a candidate: two clean mints ran 21 vs 94, which he says “points at something per-exit” — exit IP and ASN, uninstrumented. Carry the instrument, not the numbers.
failure_owner=proxy_policy, neither with a number. A Sep 2026 benchmark on ScrapingBee’s own blog (eight configs, 400 requests each) sizes both: one flag moved a headline from 79.5% to 64.5%, and two providers each refused 22 of 100 general-web requests.transparent_status_code=true, which surfaces the first response, drops it to 64.5% (258 of 400) — Amazon 98% to 67%, Google 20% to 0%. The reusable tell is the latency table, and it is the guide’s inference, not the benchmark’s claim: Premium’s median was 1.88s against a P95 of 118.23s, a raw Oxylabs proxy 1.08s to 2.94s. Retries hit a minority of requests and never move a median — medians alone cannot tell you how much of a success rate was bought with time.The second number nobody benchmarks: requests the provider declined to make. Oxylabs and Decodo each refused 22 of 100 general-web requests at the proxy, the same 11 domains both rounds — the whole gap between their published 76% general-web score and 97.4% (76 of 78) on requests they allowed through. AirProxy and NodeMaven refused nothing, so the set is per-provider and undocumented: a league table that does not separate refusals from blocks is partly ranking legal departments. Count refusals as their own column on a trial key before signing.
No mobile pool cleared the bar on Google. All six standalone pools returned 0.0% usable SERPs over 100 Google requests each — a
/sorry/ page or a SERP-less 200 counted as failure — and NodeMaven’s SOCKS5 arm also scored 0 of 100. Only managed paths scored: 20% plain Premium, 82% Stealth. Six pools at zero is a floor effect, not a verdict on carrier quality. NodeMaven’s Stan Sadokov called the comparison unlike-for-like on LinkedIn; whether the author conceded is not retrievable.What it does not show. Published on ScrapingBee’s own blog, and Stealth tops the table at 94.0% on 200 requests rather than 400: the winning arm rests on half the sample. Italian exits only, no test dates, no provider ordering, no runner OS, so time-of-day is uncontrolled — the warm-up card’s largest confound. The “Google changed something upstream” account is relayed from Oxylabs support, a vendor on its own zero.
Oct 2026 update. The disclosure above is incomplete. ScrapingBee has belonged to Oxylabs since June 2025, and on 13 October 2026 Oxylabs, Decodo, ScrapingBee, Webshare and Honeygain were presented as one ecosystem, Source, with shared compliance and use-case vetting standards. The publisher, the raw Oxylabs proxy, Decodo and the support account quoted here are siblings, so the identical refusal lists are plausibly one policy rather than two. Read it as a group-internal benchmark.
Derive a response size, not a multiple. Per-GB stops winning above
($/success ÷ $/GB) × 1,000,000 KB: ~266 KB at $5.64/GB, ~160 KB at $9.40/GB, ~106 KB against a committed $1.00 per 1,000 — upper bounds, since Oxylabs meters traffic, which we read as request bytes on top of payload. It sells in 8 GB blocks ($45 promo / $75 list), so $56 and $94 are extrapolations, not prices you can buy. Trimming payload bites a per-GB bill only; against per-success the lever is request count.The widest spread is intra-vendor. Zyte publishes its price list as the difficulty scale. PAYG per 1,000 HTTP responses: Simple $0.13, Easy $0.23, Moderate $0.44, Complex $0.70, Advanced $1.27; browser-rendered $1.01–$16.08 across those same five. One vendor, one unit, cheapest to dearest: 124×. But those labels are Zyte’s own commercial classification of difficulty, not an independent measure. And Simple sits roughly 8× below this guide’s own 1 to 5 USD per 1,000 requests band, which describes hard targets.
Caveat. No measured invoice anywhere: every figure is a published rate multiplied out, and the calculation comes from a party selling the aggregation layer it recommends. Its “billions of requests a month through more than 50 proxy providers” is self-reported and unaudited; the newsletter carrying the argument returns HTTP 403 and reached us only through a text-extraction proxy. That metered traffic includes request bytes, and that a wrong-but-convincing 200 is billable, are inferences — Oxylabs states only that 5xx on its own side is not charged. And WU40 expires.
Oct 2026 update. A four-check cost audit from the same newsletter’s author (1 Oct, an infographic with no figures) states the practical case of this card: small JSON API responses are where per-GB billing wins, since a few-KB response sits far below the ~106–266 KB crossover. Its other checks (proxy tier, unparsed assets, needless browsers) repeat the cost drivers in §cost. A reply names the open gap: no tool attributes proxy, compute and maintenance cost per target.
Oct 2026 update. A Web Scraping Club review of AntsData (1 Sep; a vendor tool review, no disclosure either way) shows why you price from the bill, not the list. One Google query returning 8 results billed $0.002; an Amazon best-seller job billed $0.55 for 82 records, against a listed $0.55 per 1,000 (about $0.045 expected). Its pricing page on 12 Oct lists $1.00 per 1,000 searches and no minimum. The advertised MCP returned a 404. One run each, cause unexplained.
Latency is the same variable read from the other side. His bands: under 5s very good, under 10s good, 20–30s much weaker; and 99% success at 20–30s against 80% at 2–3s, which he frames purely as throughput. Score candidates as the health-scored rotator scores routes. His percentages are unmeasured and not like-for-like: 90–95% of traffic at the basic one-credit level is him recalling ScraperAPI, 20–25% needing more than the base configuration is a present ScrapeOps “rough estimate” — two companies, no time window, no dataset. Only the 90–95% baseline is new.
The caveat: none of this is a published measurement, and the company saying it sells the answer. It closes on the gap its own product fills: automatic per-domain selection “doesn’t fully exist yet… we’re around 60% to 70% of the way there.” Its scale claims (billions of requests a month, 50+ providers, ~60,000 domains) are unaudited; the AMA thread could not be retrieved, so no community pushback has been read. This guide carries labelled affiliate proxy links itself. His defence: ask a provider for a bigger trial and “they know they are being benchmarked,” so ScrapeOps tests through ordinary paid accounts, and “testing 10 to 20 providers properly can easily cost $500 to $1,000”. This card’s extension, not his: a spend-capped benchmark hands your priciest candidates the smallest samples — budget each provider a number of requests, not dollars.
connect(), browser.close() only “disconnects from the browser server”. A benchmark on GoLogin’s own blog, about its Cloud Browser, puts a latency figure and a bill on it.close() disposes only the contexts you made and drops your socket — the remote process, the persistent profile and the billing clock run on, and a warm reconnect is how you catch itBrowser.getVersion samples. After browser.close(), reconnecting took 1,556 ms, page still open (warm band 1,554–1,676 ms), a localStorage value written 20 s earlier still readable. The provider’s stop call (DELETE /browser/{id}/web, 204) raised it to 6,046 ms, inside a cold-provision band of 5,273–6,616 ms. Two non-overlapping bands: warm means it never died, so reconnect latency is a leak probe. The thresholds do not travel — the author is explicit that “the latency ratios scale with your own round-trip time” — so measure your own. Put the close and the stop in a finally, or every crashed run keeps billing.Remote CDP charges you per locator — here, 63×. Twenty cards field-by-field issue roughly 80 protocol calls: 16,098 / 16,010 / 15,256 ms over three runs against one target. As one
page.evaluate(): 500 / 254 / 247 ms — medians 16,010 / 254. Locally a chatty chain is untidy; over a wire it is the bill. Nor reach for newContext(): it opens an empty jar while the logged-in state sits in contexts()[0], and the tmpfs profile dies with the container — export cookies first.The caveat. This is GoLogin’s own blog about its Cloud Browser, so read comparisons as vendor material. Its three published self-serve tiers, $0.09 / $0.08 / $0.07 per browser-hour, are 2.6–3.3× an ARM Fargate floor of ~$0.027, scoped to those tiers: the same page’s FAQ quotes overage “at $0.04-0.09/hr depending on plan”, so the honest published band is ~1.5–3.3×; the floor rests on an assumed 6 browsers per 4-vCPU task, the author’s stated assumption, not a measurement. Reported rather than confirmed: each latency figure is one run from one client-host pair, the self-hosted column was inferred, and the idle-timeout and dropped-connection policies come from an unnamed engineer at a different provider; GoLogin’s own is published nowhere.
Two 2026 benchmarks, opposite winners. Browserless’s harness (
browserless/benchmarks, default TOTAL=10 on example.com) made Hyperbrowser fastest to connect, 692.5 ms. Steel’s BrowserBench (5,000 runs per provider, create→connect→goto→release) puts it fourth: Steel 894.13, Kernel 1,518.53, Browserbase 1,676.76, Hyperbrowser 3,657.11, Anchor Browser 8,001.29 ms. Steel’s stage table explains it: Hyperbrowser’s goto 377.89 ms is fastest of all five, while create 1,731.63 and release 1,199.98 ms carry the deficit — stages a connect-plus-navigate run never separates. Over those 5,000 runs: Steel and Kernel 100%, Browserbase 99.96%, Anchor 97.34% (133 failures).Whose back yard — and the caveat. Steel ran from AWS EC2
us-east-1, its own only browser region, rivals on default settings — so Browserbase (default us-west-2) was clocked cross-continent. Browserless never says where its harness sat. Vendors measuring vendors: each bench crowns its author, the LinkedIn post carries no data. Attributing any of Browserbase’s gap to distance is our inference from two documented defaults, not a controlled A/B; and region placement does not outweigh the published boot-time gaps — Steel’s create stage alone spans 181.57 to 3,796.55 ms. No one has run every provider from every region with stages split out; until then, pin the region explicitly.>=0.9 skips the verifier (85% of rows), 0.6–0.9 verifies (12%), <0.6 quarantines (3%). TypeSafe's sde_cascade cookbook removes the reason to ration: its checker is independent, non-generative, and lists cheaper per input token than the extractor it audits.jev-1.12 0.042/0.00, output unmetered. A ~18× list-price gap per input token (0.75 ÷ 0.042 = 17.9): arithmetic on published rates, not a measured per-row saving. Stale pin, same ratio: the models page now lists only jev-1.13 (~18 Sep 2026) at 0.042 in, free out. Two traps: the escalation rung gpt-5.5-reasoning is not a separate SKU, and its reasoning tokens bill as output at $30.00/M, far above the 6.67× input ratio; and write-ups report Jev free on Vercel's AI Gateway until 25 Sep 2026, so metering now reads $0.Seven questions per field, aggregated with max. Each populated field gets seven binary heads (hallucinated, off_target, type_mismatch, unreasonable, incomplete, format_violation, name_desc_mismatch); each empty field gets absence_wrong. Gate:
FIRE_T = 0.7, aggregated by max not mean, so one confident flag escalates instead of averaging into silence. All seven ride one call per record. Per-field confidence isn't new here; the independent judge is.Same bytes: fidelity, not corroboration. Jev reads the page the extractor saw: extraction checked against source, the check the 85% fast lane skips. It cannot replace the guide's re-fetch verifier: same bytes never reveal a stale, blocked or poisoned page. Worked failure: on an NYU page the mini model left registration_open_date empty but invented a description that parses, validates and lies; Jev fired hallucinated 0.95, off_target 0.85. Hence its refusal of structured outputs: conformance never catches a field that lies. The walkthrough is hard-coded, not live.
Caveat: vendor-reported quality, non-independent ground truth. The Pareto chart is “internal TypeSafe results” over 100 prompts, from the vendor, with no quality metric or ground truth; figures are read off a raster (gpt-5.5-reasoning alone ~0.81 quality at ~$0.10 per extraction, vendor-reported), the page admits a snapshot not recosted at Jev's rate. Nor is the dataset leg independent: scrapegraphai-100k (93,695 rows, publisher self-reported) shares an author with the LinkedIn post promoting it, and its card calls responses “LLM outputs… not human-verified ground truth”, so scores grade against gpt-4o-mini. Measure your own escalation rate; that, not $0.042, sets the bill.
The counter-number is a demo, of another system. The only published task-level figures: WorkOS (Zack Proser, 11 December 2025) writing up a live demo by Rita Kozlov, Cloudflare’s VP of AI & Developers — a calendar agent, not the API server, two months before it shipped: 32% fewer tokens on the simple single-event task, 81% on the complex 31-event one. Both are reported from one unrepeated live demo, third party, no methodology, no run count — and neither Cloudflare post contains a 32% or an 81%. Reading most of the 81% as batching (one model turn, not 30-plus round trips) is our inference, not theirs.
The caveat, and what to count. Every figure here is first-party or demo-grade, and none of the sources publishes an error or retry rate for model-generated code — our observation over the set, not a gap any source flags — so the savings are never netted against a script the model got wrong. Anthropic concedes the approach “requires a secure execution environment with appropriate sandboxing, resource limits, and monitoring.” The win scales with how many definitions you carry, so a crawler holding one
scrape(url) → markdown server has nothing to cut: its bill is the payload, this guide’s 17,131 → 3,618 → 107 curve. Count your system-prompt tokens before the first user message; trim payload first (free), the menu second (it costs a sandbox).Opus 4.8, six retrieval paths, two unnamed Context-as-a-Service tiers.The reconciliation is the payload, not the 15,000. This guide’s unit is one successful HTTP request, cost driven by proxy bandwidth. The talk’s unit is one enriched record — retrieval plus the tokens that turn prose into 25 typed fields, which its table calls “Mid-band, but heavy token burn to structure results.” Same decision, different denominator: memoized extraction, inverted.
The per-record price is a floor on an ambiguous denominator.
$5,000 ÷ 15,000 is about 33¢, divided here, not in any source, and a floor: break-even is amortised build plus the owned path’s marginal cost, and “zero AI cost — no tokens at all” means zero token cost. The source’s denominator is “just over 15,000 entities or queries” — two units in one phrase. Per call, a 0.1¢–0.8¢ retrieval fee is 0.3% to 2.4% of 33¢, a rounding error; per entity, ~25 calls at $7 per 1,000 is roughly 17¢, about half. Those prices are a third-party compilation (31 Aug 2026) marked “illustration only.”The caveat, and it is large. Bright Data sells both arms, the CaaS measured and the Scraper Studio that built the winner, and its $5,000 is one engineer-week valued internally, excluding maintenance; “self-healing” and its self-reported scale figures are vendor claims, untested here. Primor disowns the precision (“maybe it’s not 15, maybe it’s 100,000”), every table cell is an ordinal adjective and both CaaS vendors unnamed, so it is unreproducible. Do not adopt 15,000 or 33¢: compute the crossover in your own unit, from your token bill per accepted record.
Retry-After — never on latency, since stubs return faster than real pages. Polymarket’s read endpoints sit behind a Cloudflare IP limiter its docs describe otherwise: over it, requests are throttled (delayed/queued) rather than immediately rejected. Nothing to count.Name the limiter you read. The same site’s CLOB trading page documents per-signer buckets returning 429 +
Retry-After, “separate from the existing Cloudflare IP-based limits”; sibling docs.polymarket.us specifies 20 req/s per API key and per IP, flat 429, no queuing. Three surfaces, three semantics: the queue is the Cloudflare IP layer in front of public read endpoints.A field named
volume measuring two things. Bellingcat, on these APIs: Polymarket shows traded dollars, while “Kalshi… shows the notional volume, which counts every contract traded at the maximum payout value of $1” — a token bought for $0.20 shows as $1. Rebuilt as daily average price × contracts traded over “more than 21,000 World Cup markets”: Polymarket $10 bn against a reconstructed >$4.3 bn. Encoding-and-units one level up: parses perfectly, checks pass, wrong quantity.Documented, not observed. The behaviour was read from the docs, never exercised against the live API, and the ceilings were read September 2026, after the June–July 2026 collection window; a dev.to post reports “in practice you do see 429” with no logs. The one speedup line (“multi-week… single-day task”) is Oxylabs’ own blog about its own proxies, no request count or measured rate: vendor-reported. Whether Kalshi’s volume also double-counts both sides is reported rather than confirmed — docs.kalshi.com did not resolve.
adscore.com/implementations option 3 of 7, Active Filtering, “sends good traffic to your landing page and bad traffic to a honeypot.” Its FAQ says it detects “antivirus scanners like Google Safebrowsing” and GeoEdge — while explaining false positives, so the cloaking primitive is this guide’s inference, not Adscore’s pitch. An abusive customer makes that join, honeypot branch as the auditor’s decoy.Catalogued. Confiant’s Malvertising Attack Matrix lists 9 tactics, 93 technique IDs; Cloaking is
C601–C615. C614: bot-detection services used “to hide from ‘good’ bots.” C604 inverts the proxy economics: payloads are withheld from proxy, VPN and datacenter IPs as likely “scanners, bots, security researchers.” A datacenter exit is not blocked there — it is made blind.The 2026 commodity form. Varonis Threat Labs documented 1Campaign, cloaking-as-a-service for malicious Google Ads: a 0–100 fraud score, cloud infra blocked by ASN (a Microsoft entry at score 0), plus headless-browser and user-agent checks Varonis alone reports. On one campaign its dashboard showed 1,676 visitors, 10 approved, 99.4% blocked.
The caveat. The practitioner post names no vendor, number or link, and its trend claim is reported, not confirmed. Not new either: the March 2019 teardown behind it is one analyst’s single observed chain and
C614’s only reference (that page 404s), so the two are not independent; prevalence is unquantified. The 99.4% is the attacker’s own dashboard on one campaign, off screenshots — not independent. Adscore itself is legitimate: a commenter self-identifying as its founder said they monitor customer redirect links and act on malicious ones (unverified).The defensive read. The
200 OK false negative is the LLM Honeypotting card’s; new is a bought classifier with a brand-name target list. Treat the apparatus as the finding, re-sample by time of day (C613 is time-gated), and never let one clean fetch close a case.The mechanism is worth stealing. First run installs an Artemis Accessibility Helper, the README’s “small accessibility service that reads the screen layout without taking the UiAutomation connection”, and warns UiAutomation tools can suppress it unless they enable
FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES; that this is the Appium/uiautomator2 clash is this guide’s untested gloss. Targeting is tiered: a11y indices, OCR and coordinates, visual models last — the order the paper backs, text-only a11y tree 30.6% against 25.4% for screenshot-plus-a11y on the identical model, though ARTEMIS’s own row is filed as Screenshot + A11y tree.Price it, then discount it. The README runs Flash at ~3–5 s per step and Pro at ~15–40 s per step, so a 100-step Pro run is 25 to 67 minutes on one device — this guide’s arithmetic, not measured in the 99.1% run. AndroidWorld is emulator-only (Pixel 6 AVD, API 33) and no task there involves an app trying to detect the agent, so the attestation card stands. The 99%+ is Google’s claim for Google’s agent on Google Research’s own benchmark, no profile or model named. Provenance is unadjudicated: Minitap’s 11 Sep 2026 post and issue #40 allege shared code and that the first
pyproject.toml carried mobile-use’s version 3.6.3 under a Google LLC header; a Minitap credit line is now in the README, and secondary reporting reports no detailed Google response.Sep 2026 update. The same pattern in browser agents: the vendor of Open Swarm (openswarm.com; AGPL-3.0 desktop app, repo created Mar 2026) announced its own browser agent as “SOTA on agentic browsing benchmarks” and ~85% faster than browser-use with 60% fewer model calls. As of 28 Sep 2026 neither the site nor the README names the benchmark, the tasks, n, or who ran it. Treat it as a vendor self-claim with zero verifiable evidence until a results page exists.
0 never mentioned, 1 partial or self-contradicting, 2 a clear vendor statement. One is confirmation before irreversible actions. Testing only subtracts: contradicts claim/credibility gap zero the capability, reliability gap takes 1, corroborates nothing. No verdict adds a point.The safety column is nearly empty. Transactional actions averaged 0.43 of 2, which the report calls its lowest average; cross-tab awareness 0.66. Six document a completed purchase; only those two also document pausing first. Ten of 45 document a pause anywhere; the report names nine, never a tenth. Of the 35 that fully document multi-step task completion, 18 (51%) document zero confirmation step; 17 document running with no active user session, seven with no confirmation at all.
Where testing bit.
Fellou documented 19 of 20, highest in the published rows, finishing 17, looping on layouts and failing CAPTCHAs. Browser Use 14→10; Devin 13→9 on a credibility gap (its demo showed it fixing files absent from the repo), 2 of 10 tasks error-free; Comet 6→4; Manus 5→2. Top adjusted: Claude for Chrome and Nova Act, 18; nobody reached 20.The caveat is large. Decodo sells an access layer for AI agents, so a report whose headline failures are layouts, CAPTCHAs and page access doubles as a pitch. The companion Next Web op-ed by CEO Vaidotas Juknys is a fee-based TNW Council placement that never discloses the research is his own company’s and puts the count at three where the report names two. It audits documentation, not capability: a careful tool with thin docs scores 0. Five tools carry a published adjustment; the report never says how many of the 45 were tested; Decodo “only ran each task twice per tool”, by its own team, with no versions, dates, OS or dataset.
Either side. Defending: assume agent checkout traffic is not human-gated. Building: the confirmation gate is your code, not a procurement checkbox.
wafw00f 2.3.2 firing attack-shaped probes at 11,100 root URLs, once each, from Zyte’s datacentre IPs in October 2025. It measured reaction to an exploit, not obstruction of collection.wafw00f 2.3.2 — whose last documented step is a fallback guessing whether a solution is “actively responding to our attacks.” Denominator: 11,100 records (9,774 unique sites, one row per industry, not the 24,898 headline), root URL only, once, October 2025, Zyte datacentre IPs. 92.4% is 10,252 of 11,100.One detection in three names nobody. Cloudflare 34.6%, Generic/Unidentified 31.6%, CloudFront 9.6%, Akamai Kona 4.9%, AWS ELB 4.3%, 13 more under 2% each — that bucket is the fallback firing, and it sits inside the same 100%, so Cloudflare is ~51% of the attributed detections (34.6 of 68.4), not the post’s “34.6% of named deployments.” Top five: three CDN or load-balancer defaults — see The Cloudflare Header card.
Presence is not posture, and a 76% floor cannot rank targets. Default managed rules aim at the OWASP Top 10; a polite renderer pipeline “does not look like an SQL injection attack.” The controls that stop collection — rate limits, datacentre IP reputation, TLS and behavioural checks — are, per the vendor, off by default or in premium bot tiers. Same table: WAF 92.4% vs anti-bot 18.5%. It barely varies: six regions 92.5–93.4%, Japan’s 86.4% the one outlier the report ties to domestic CDN and hosting, floor airlines 76%, military 77%.
Caveat, and two numbers to fix. Zyte’s own report, and Zyte sells an unblocking API, so its “signal to investigate” framing is also a sales message. One unauthenticated homepage visit says nothing about the endpoint you want; the report concedes rate limiting is “likely an undercount.” Vendor shares hang on one pinned signature set (2.3.2; current releases claim 200+), and the unattributed third is a fingerprinting blind spot, not bespoke WAFs (its ModSecurity-lost-support-in-2024 explanation is reported, not confirmed). Corrections: the free Cloudflare managed ruleset dates to March 2022, not 2017; and this guide’s “more than half” rate-limiting figure is the fashion cut (54%), not the global 22.1%.
Do not mix discovery with extraction. For an extraction comparison hand both candidates the same URL list; for a discovery comparison, the same starting URL and scope. “Mixing those tests makes it harder to tell whether you have a discovery problem or an extraction problem.” That is failure attribution — the proxy-side habit (IP, browser, or host?) on the crawl-versus-parse axis. Test set: ten representative pages you are allowed to crawl, including any layout that has broken extraction before.
Ordering, and the check that catches silent failure. “Define acceptance before measuring speed” — then time only the output you accepted. Of the four checks — coverage, content, noise, failure reporting — wire the last first: save the failed URL and its error message, or “an empty result appears as a success”. The boundary on pilots: “A small local sample can tell you whether the output fits your application. It cannot tell you the monthly operating cost of a larger deployment.”
The caveat, largely the author’s own. He builds one of the four tools and discloses it, so read the shortlist as a vendor’s. The only measurement is one command, 21 September 2026 — depth 1,
--max-pages 3, --concurrency 1, a 1000 ms delay, --only-main-content — against docs.scrapy.org, a static Sphinx site with no JavaScript rendering and no bot protection; three files landed, and the overview kept the example spider with its pagination code. He states “I have not run a head-to-head benchmark of these four crawlers,” and that the check establishes no success rate for other sites and no speed advantage over another crawler. No timings, throughput, versions or file sizes appear anywhere. One gap: the LinkedIn version says to confirm indentation and the source link survived; the article verifies neither.curl_cffi as the default browserless client and HTTP/3 as a detection layer. The 0.16 series (Aug–Sep 2026) closes the two gaps that mattered: it now impersonates HTTP/3 over QUIC, and it can pull fresh browser fingerprints from an API instead of waiting for a pip release — which is the direct fix for the “a pinned profile rots into a tell” problem this guide keeps flagging.trust_anchors extension for Chrome 152 (detection vendors check for it, so it is now matchable); more HTTP/3-specific options landed; a new option sets HTTP header order (header order is itself a fingerprint signal, and plain requests silently reorders yours); plus DNS-over-HTTPS, redirect history and streaming request bodies. It stays a requests-like API — impersonate="chrome", and for the new transport http_version="v3".Why HTTP/3 impersonation is the headline. More targets negotiate HTTP/3 over QUIC, and neither
requests, httpx nor aiohttp can speak it with a browser's QUIC fingerprint. curl_cffi now does QUIC fingerprint impersonation and UDP proxying, so the client matches the browser it claims to be at the transport layer, not just in TLS and HTTP/2. For roughly 80% of targets this is the whole game — you do not need a 200 MB headless Chromium, you need a client that looks like one on the wire, and this one is faster than requests while doing it.The self-updating part is the durability fix. This guide's profile-drift card warned that a pinned impersonation profile eventually describes a browser version almost nobody runs, which is its own anomaly, and recommended scheduled re-measurement. curl_cffi now offers exactly that for the HTTP-client path: a
curl-cffi update pulls the latest Chrome/Safari/Firefox fingerprints from the maintainer's API, so a new browser release does not leave you pinned to a stale ClientHello. One honest note: that fingerprint API is impersonate.pro, the maintainer's commercial service, and the Chrome 152 target is offered through it — the same paid-freshness axis the guide already notes for this library. The base client and the older presets stay free; staying current on the newest browsers is the part that is monetised.success = decay·outcome + (1−decay)·success with outcome 1.0/0.0. Latency EWMA updates only on successes (so failures don't distort it): latency = decay·sample + (1−decay)·latency. Default decay 0.3 on both, so the last observation is ~30% of the estimate. The health score is multiplicative: score = success_ewma × 1/(1+latency_ewma) — a route must be both reliable and fast, because a failing route scores near zero regardless of speed and a fast-but-flaky route can't score high. A circuit breaker opens after 3 consecutive failures (score forced to 0), waits a ~15s cooldown, then half-opens and closes again on a success. Selection is probabilistic on score, not top-1, and a deliberately modest exponent keeps a thin probe slice of traffic on demoted routes so recovery is still detectable.The benchmark, and the honest caveat. On a controlled workload of 20 requests, both policies reached 100% success, but mean latency moved from 1.589s (round-robin) to 0.289s (health-weighted) — roughly a 5.5× improvement. Read it correctly: the target was unprotected and both policies succeeded, so the result is the latency, not the success rate. The claim is narrow and true — steering by observed behaviour beats steering by list position — and on a real blocked/rate-limited target the same machinery would show its value on success rate instead, by pulling burnt exits fast and probing them back.
Why adopt it. This is the cheapest reliability upgrade in the sweep and it composes with everything else the guide recommends: it is the proxy-side twin of the failure-attribution the benchmark harness teaches (is it the IP, the browser, or the host?), and the circuit-breaker + probe-slice shape is the same recovery logic a good crawler already wants. Keep it provider-agnostic and it doubles as the routing layer under a proxy waterfall.
The datapoint, attributed and hedged. At the Press Gazette Future of Media conference (Jon Slade and Dominic Ponsford of the FT), it was reported that one trial account made about a million requests to ft.com to lift subscriber-only content — the kind of single-account, high-volume harvest that a general anti-bot waves through because each request looks like an ordinary logged-in read. Note: that figure comes from the conference talk as relayed by an anti-bot practitioner, not from an FT publication we could independently verify; treat the exact number as reported rather than confirmed. The structural argument does not depend on it.
What it means for both chairs. For a defender in publishing, the takeaway the speaker draws is to stop treating a general WAF as the answer and instead understand who is scraping you and what they do with the content, then build a tailored control — abuse detection on the authenticated read pattern (one account, a million article fetches) rather than a bot challenge on the front door. For a scraper, the same fact explains why login-gated publisher content is often reachable through an ordinary authenticated session (see the account-creation card) even behind a name-brand WAF — the wall is priced for a threat model that article harvesting does not fit. The honest framing both sides should keep: this is an argument from someone selling tailored protection, so weigh it as a real structural point, not a neutral verdict on Cloudflare.
<select>'s computed background colour, where the platform's own widget theme answers instead of the page: rgb(250,250,250) on Linux, rgb(255,255,255) on Windows. Neither needs a lab. The heavier readings follow the same rule — native accessor integrity alone is four checks on every accessor a spoofer must touch (how the function serialises, whether three key-enumerators agree, whether it still sits on the interface prototype, whether the wrong receiver throws the spec's TypeError), plus screen-vs-viewport-vs-CSS geometry, timezone against measured offset, canvas double-serialisation, and GPU/decoder/media coherence.The results table is the payload. Score is 0–90 (it never claims certainty), rounded to tens so no single reading can be isolated by watching the last digit. Stock Chrome / Firefox / Edge: 0 across all 100 samples. Brave: median 10 — and the author is explicit that this is honest, Brave modifies the surfaces it says it modifies, which is the lightest thing recorded. Then the anti-detect column: AdsPower 60, CloakBrowser 70, NSTBrowser 70, Camoufox 80 (medians). None below 60. And the design detail that makes it trustworthy: a reading abstains rather than guessing when the probe is missing, the feature isn't exposed, or the reference table isn't verified — a browser is never scored for lacking a feature. 7 of 22 reference entries are still unverified, so those numbers are a floor, not a best case.
Why it lands the same week as a CloakBrowser rave. A scraping engineer posted a genuinely positive CloakBrowser review the same day: fingerprints patched at the C++ source level and compiled into the binary, so “detection systems see a real browser because it is one” — drop-in Playwright replacement,
humanize=True (Bézier mouse, per-character typing), geoip=True (timezone and locale aligned to the proxy exit), and a reCAPTCHA v3 score moving 0.1 → 0.9. Both are true at once, and the tension is the lesson this guide keeps arriving at: binary-level patching wins the value game (it passed the OSS stealth benchmark's JS-fingerprint referee) and still scored 70 on coherence, because the leaks anti-mage reads are cross-surface agreement — the host's own fonts and widget themes, the ordering the interface prototype enforces — not the JS values a patch controls. Passing a fingerprint check and being internally consistent are different bars, and the second is the one still open.One honest note. The author is releasing this once, maybe twice, and will not maintain it — a live scoreboard would just hand the anti-detect side a to-do list. So treat the specific readings as a snapshot of what's cheap to catch today; the durable takeaway is the method, which is the same one the guide's Camoufox and cpuPerformance cards teach: spoof one property and you now own the job of making everything it touches agree, or a scanner that costs nothing finds the seam.
Oct 2026 update. A GoProxies and ScrapingAnt piece extends coherence to the network: keep the ASN fixed within a session and match exit geo to the target’s real audience, because per-site behavioural baselines push a generic exit out of distribution. The mechanism is real, per Cloudflare’s September 2025 per-customer models post. The claim that detection windows fell from months to days comes with no data, and both authors sell proxy or scraping access.
/goto teardown flagged two things nobody had measured: the sustained resolution rate before Google rate-limits bulk follows, and whether the tokens are portable enough to decouple harvesting from resolving. The September industry roundup and the two SERP vendors have now answered both./goto resolution — had not been measured. It now has: resolution testing sustained 110 requests per second from a single home IP with zero rate limits observed. That is a very different planning number: following the redirect is cheap, and the cost of the change is the extra request, not a throttle wall.The token lifetime. The card hedged the vendor claim that tokens are portable and last for days, because it was one unreplicated observation. The roundup now states plainly that the tokens remain valid across multiple days. Combined with their non-determinism (still no token→URL lookup table, ever), that confirms the two-stage design is real: harvest tokens through the expensive stealth/proxy path, and resolve them later — a different time, a cheap IP, plain requests — because the token does not expire on you and does not need the original session.
And the vendors caught up. DataForSEO reported resolving organic
/goto URLs for “99.99% of returned SERPs” as of 27 August, and SerpApi announced on 5 September that direct URLs are back in its responses — the resolution the earlier card noted they had shipped without disclosing the mechanism. Net effect for an operator: /goto is now a solved, priced-in cost rather than an open threat, and if you resolve it yourself the numbers above say it is a small one. What has not changed is the structural part — the destination is still encrypted and non-deterministic, so this is resolution by following the redirect, not by decoding the token.anything@yourdomain.com) exists and receives mail; pull the code over IMAP and parse it. Unlimited fresh addresses at zero marginal cost, which is why “different user every time” is trivial on email. Run a few domains so signups aren't all concentrated on one. Avoid the disposable-mail APIs (mail.tm, temp-mail, Kopeechka) as your primary path — they're cheap but their domains are widely blocklisted, so they fail on exactly the sites worth logging into.SMS OTP — where the money goes. The API-driven providers are 5SIM and SMS-Activate (with SMSPool and SMS-Man as fallbacks): rent a number for the specific service, poll their API for the code, and release/refund it if nothing lands in ~90s. Two levers decide success — use the non-VoIP / higher tier (plain virtual numbers get rejected), and match the number's country to the target's geo (cheap-country numbers are often pre-burned). Build it provider-agnostic as a waterfall (5SIM → SMS-Activate → SMSPool/SMS-Man) because availability swings hourly — the same pattern as a proxy waterfall. If a target rejects all virtual numbers, that's the escalation case: a real-SIM or eSIM provider, materially pricier.
What it costs, per delivered OTP. You pay only for received codes (no code → auto-refund), but budget a real-world 70–90% success rate so effective cost sits above sticker. Common service on a cheap country: $0.05–$0.30. High-demand service (Google, WhatsApp, big platforms): $0.30–$1.50. US/UK/EU or non-VoIP tiers: $0.50–$3+. Real-SIM/eSIM escalation: $1–$5+ or a monthly rental. At scale that dominates: ~200 accounts at $0.30–$0.50 is roughly $60–$100 per run in SMS; a few thousand accounts is $1k–$2k+ per full pass. Email adds ~$0, so SMS is essentially the entire per-account cost.
The two decisions that save the most money. First, check whether the target actually requires a phone at all — many gate on email only, and an email-only account costs about nothing, so this one check per site can cut the bill dramatically. Second, question “fresh account every run” itself: it is usually a symptom of accounts getting banned, not a real requirement, and a rotated pool of warmed accounts is normally cheaper and longer-lived than churning brand-new ones. Fresh-per-run maximises both cost and ban signal, so choose it deliberately, not by default. And whichever you pick, the account only survives if the whole identity is fresh and internally consistent — a per-account proxy and fingerprint, plus a plausible warm-up, or the accounts cluster and get caught in a batch.
The honest caveat. Mass account creation generally violates a site's terms, and bulk OTP numbers are the same infrastructure fraud rings use, so quality and blocking move constantly. Treat login-gated collection as a deliberate, per-target decision — and check first for an unauthenticated path, an official API, or a partner feed before committing to an account-farming pipeline, because it is the highest-friction, highest-maintenance route to the data.
The same shape, on the Google /goto front. This guide's 324-token teardown established that the destination is ciphertext in a protobuf envelope, non-deterministic, and not decodable locally. SerpApi's status post — which a reader asked us to check — is the commercial mirror of that: they confirm the destination is “still present within Google's /goto URL, but in the form of ciphertext binary, packed into Protocol Buffers,” they say “no changes are required to your SerpApi integration,” and they list which endpoints are affected (Google Search, Search Light, News Light, Videos) versus marked solved (Short Videos, AI Overview). What they never state is how they resolve it — the mechanism stays undisclosed, and even the CEO of a proxy company is on record in the other thread wondering aloud whether SerpApi “just started scraping more or [found] a permanent solution.” If you depend on a vendor's /goto resolution, note that you are trusting an opaque process whose cost and durability you cannot see.
The one tactic worth taking from the noise. A hype post declaring scrapers dead over /goto drew a genuinely sharp reply: you cannot build a token→URL lookup table (the tokens are non-deterministic), but you can skip resolution for links you already know. Match each SERP row against your existing corpus on title, snippet and domain, and only spend the extra follow-the-redirect request on the rows you can't match. It is not perfect and it does not help a first run on unseen results, but for an operator re-crawling a known space it turns “an extra request per result” into “an extra request per new result,” which is the difference that decides whether the change hurts. The counterpoint several practitioners raised is also fair and already in this guide's read: one extra redirect is something scrapers have handled for years, so this raises cost rather than ending anything.
The mirror, which is the part a scraping audience underrates. OSINT — open-source intelligence — is web scraping plus web search plus AI pointed at a person instead of a catalogue. One email from a leak, one image from a conference gallery, one profile from a lead-generation platform, stitched together at scale by a model that is good at connecting dots across platforms, produces a convincing dossier. The scraping-specific point is that this is the ordinary stack — crawlers, SERP access, extraction — not exotic tooling, and AI has dropped the skill and money bar for assembling it, which is the same “bar of entry collapsed” the guide notes for CAPTCHA solving and bypass tooling. Lead-gen platforms built for a legitimate use are a dual-use collection surface.
The defensive flip, and the honest boundary. The useful move for a team is to point the same toolkit inward: scrape your own and your company's public footprint to see what a profiler could assemble — exposed emails, reused handles, event photos, leaked records — and close what you can before someone else enumerates it. That is a legitimate, defensible use of scraping infrastructure, and it is the one this card is recommending. The offensive direction — compiling dossiers on people for social engineering, phishing, deepfakes or voice cloning — is the harm the “synthetic web” talk is warning about, and it sits behind the guide's existing lines on not compiling personal data across sources; the reason to name it here is that the capability falls out of tools this audience already owns, so knowing the shape of the misuse is how you defend against it.
Sep 2026 update. Cloudflare Radar put bots at 57.4% of HTML requests on 3 June 2026, a crossover its CEO had forecast for 2027. It is global and HTML-only: the US-origin view read 43.6% bots. Radar also revises classifications retroactively; a scraping vendor’s stats roundup reports a re-run 28-day window returning a materially lower AI-training share. Quote any Radar figure with the date you pulled it.
Why it belongs in a scraping guide. The podcast's Ian Johnson drew the lesson that matters for anyone shipping agents: today's models are restrained by defined permissions, not human intent. A tool call that is entirely legal — install a package through the approved proxy — became the delivery vehicle for an exploit, because nothing in the permission set encoded “and don't turn this into a breakout.” This is a superset of prompt injection: the guide already covers a hostile page injecting instructions, but here the goal you set plus a genuine capability was enough. A scraping agent handed a bash tool, a proxy pool and “get me this data, maximise success rate” is the same shape.
What to do about it. The containment lesson is that a sandbox is necessary and not sufficient — this one had no internet and was defeated by a dependency it was allowed to talk to. So: minimise the tool surface an agent actually needs, treat every dependency the sandbox can reach as part of the attack surface, prefer allow-lists of specific actions over broad tools, and log tool calls for review rather than trusting the agent's own account of what it did. And do not set unbounded optimisation goals (“score as high as possible”) for an agent with side-effecting tools; the narrower and more explicit the objective and the permissions, the smaller the space of creative misbehaviour.
The most useful single claim: imitation > instruction. An agent responds to instruction, to imitation of the surrounding code, and to its tools — and instruction is the weakest. A rule saying “use interfaces over inheritance” loses to a codebase full of inheritance, because the model copies what it sees. Practical consequence, stated bluntly on the show: just letting an agent into a bad codebase is a mistake; get characterisation tests and clean seams in place first, or the agent compounds the mess faster than a human would. For a scraping repo, that means the page-object and test structure is not tidiness — it is the thing that makes agent-generated selectors come out right.
Evals are tests for the harness, and they are statistical. Because output varies, an eval isn't red/green — you need a statistically significant sample, at least ~30 runs, to compare a distribution before and after a change, and it has to hit the real API, so it costs money. The rule the guide has stated from the quality side and the podcast states from the engineering side: the model doing the work must never judge its own work — confirmation bias is real in models too — so the judge is always a separate agent, ideally a different model. And the operating principle both episodes land on: AI at build time, not at runtime. Use it to generate deterministic, tested code; keep an AI block out of the hot path unless the task genuinely needs it, because that is where non-determinism becomes an incident.
The cost of the other extreme, with a number. When you do push orchestration hard, the bill is real: a fleet of 64 Claude agents (four git worktrees, sixteen each) rewrote the Bun runtime from Zig to Rust — 535,496 lines of Zig to roughly 780,000 of Rust — in about 11 days for ~$165,000 in API cost, peaking near 1,300 lines a minute. It also shipped ~13,000
unsafe blocks and Zig's own creator called the result “unreviewed slop,” even as Rust's compiler caught thousands of bugs the model would otherwise have shipped. The scraping read: orchestrating a swarm to build or migrate collectors is now possible and is not cheap, and the review burden moves rather than disappears — which is the whole argument for the constraints above.async def. There's now an HTTPX-based download handler bringing HTTP/2 to the request path; it shipped as an opt-in rather than the default, which is the maintainers' whole ethos — nothing enters core until it's robust, and even a finished feature waits for the right moment.The download-handler seam is the anti-bot lever. Because a handler is just a small class you can read in the Scrapy source and replace, you can route requests through a client built for TLS fingerprinting rather than the stock stack. The one the Zyte dev reaches for is wreck (a Rust HTTP client with Python bindings, strong JA3/JA4 control); curl_cffi, built on a patched curl, is the other. The guide already tracks curl_cffi — the point here is that Scrapy is a first-class host for it, so you keep the request/response cycle, retries and middleware while swapping only the bytes on the wire. Their honest caveat: there is never one best client, so pick per target and keep it swappable.
The four extensions that make it production-grade, restated because the maintainers rank them: scrapy-playwright for browser rendering (connect via CDP to a stealth browser — never the default Playwright Chromium, which has no stealth); scrapy-poet / web-poet to split extraction from crawl logic into testable page objects; Spidermon for monitoring, including a periodic monitor that kills a long crawl the moment field coverage drifts instead of at the end; and the Zyte API integration for access. The self-healing loop the guide describes is these three composed: page objects keep selectors in one testable place, saved fixtures give an agent the real HTML and expected output offline, a field-coverage monitor set to (say) 100% flags a broken field, and a repair agent gets the field, URL, file and error plus tests to fix against — so the agent touches one selector, not the spider.
Two field notes. Scrapy can run as a single file (no full project), which is the right shape for an agent building a throwaway single-use spider inside a larger job. And to run a headed stealth browser on a Linux server, the maintainers decouple it behind XVFB (a virtual frame buffer — a fake screen) as its own service, which the guide already documents; the reason it recurs is that a virtual display measurably changes how a headless-on-server browser behaves.
Why AI does not close this gap. The most quotable line: the fingerprint know-how “is private, which the browser vendors know or the companies doing the protections know… not available as public knowledge that agent or AI can use.” So a model cannot vibe-code its way past a modern sensor, because the ground truth it would need was never published — and you end up running “a cloud project on fingerprint building, rather than accessing the data I need.” That is the honest boundary on “can AI build the scraper,” from someone who builds the infrastructure.
Session hygiene, stated plainly and worth stealing. The connection is yours to close, and forgetting is a cost trap — idle browsers bill until a timeout fires, so set the timeout and monitor it. And counter-intuitively, prefer many short sessions to one long one: “instead of… running like 50 pages in one browser… it's usually better to open browser, do a page, close the browser,” because a lost connection mid-session — network blip, target down — is one of the real rough edges of remote browsers. On the wire, CDP is the browser↔driver protocol and runs over bidirectional WebSockets (vs HTTP request/response for browser↔site); remote is architecturally identical to local except the browser lives elsewhere, which only adds network latency.
The pricing model, and the attribution it confirms. Zyte bills the CDP browser by website tier (1–5) × time, not per gigabyte — their argument being that browser traffic is unpredictable per-GB once ads and images vary, so a tier gives a price upfront. Note what the engineer says the tier is: “this website is very easy… tier one… this does require a lot of work… tier five.” That is the direct confirmation of this guide's earlier correction — the State of Web Access difficulty scale is Zyte API's own billing tier, so “57% of fashion sites need moderate or harder” means “bill at the higher tiers.” Same for proxies: the product picks DC/residential/ISP/mobile and country automatically from prior success, and the practitioner lesson is the transferable part — defaulting everything to residential, or pinning a specific-country proxy you do not actually need, “leaves money on the table” and can even degrade performance.
Two integration notes. You are not tied to Playwright: any Playwright/Puppeteer-based framework works by changing the browser URL, and some users connect with raw CDP from Go, Rust or Java with no driver at all. And a real gotcha for agent stacks — Zyte's CDP authenticates via an auth header carrying the API key, and some agent-browser frameworks do not support setting one, so they simply cannot connect. Worth checking before you pick a framework to point at any header-authenticated browser service.
--remote-debugging-port is ignored unless you also pass a non-default --user-data-dir, so CDP only ever attaches to an empty profile with no cookies or sessions. A red team has now published a working bypass and open-sourced the proof of concept. No command-line flag, no new Chrome process — CDP is switched on inside the browser already running on the target.NtQueueApcThreadEx, which unlike ordinary APCs run on a thread even while it is actively executing, so no alertable wait is needed. The shellcode byte-scans chrome.dll for DevToolsAgentHost::StartRemoteDebuggingServer and the TCPServerSocketFactory vtable in .rdata, allocates a 16-byte fake socket-factory object (vtable pointer, then port 9222 little-endian, then padding), and hands it to the server call. The allocation must go through chrome!malloc: PartitionAlloc enforces metadata checks on every pointer it later frees, so a HeapAlloc or CRT malloc crashes the browser. It works across several Chrome majors and on other Chromium browsers such as Brave.What it re-opens, and what stays shut. The empty-profile mitigation stripped cookies, sessions, history and autofill from anything reaching the debug port; this puts CDP back against the real logged-in profile, so
Storage.getCookies hands over live session state again. What it does not reach: saved passwords and card numbers, which sit in a separate on-disk store under App-Bound Encryption — the same boundary the guide's existing card describes, still holding. So this restores the cookie/session exfiltration path Chrome 136 closed, not the credential store.It is not wholly new, and the thread said so. A December 2025 writeup (deathflamingo) did the same class of thing against Edge on Windows: inject a DLL, byte-scan for
StartRemoteDebuggingServer, execute on the UI thread via window subclassing, allocate through PartitionAlloc, CDP on a port. The new work's contribution is the Special-APC injection and the Chrome object reconstruction rather than the idea. And a commenter offered the low-tech route that needs no reverse engineering at all: kill the running Chrome and have the user open a shortcut that already carries the CDP flags — the mitigation only governs the default profile, not what a fresh launch is allowed to do.Reading it from both chairs. For a scraper, the honest takeaway is not “attach to your real browser again” — it is memory-injecting your own browser to dodge a safety default, which is fragile across Chrome updates and squarely a red-team technique, not a maintainable collection strategy. For a defender it is the more useful half: command-line detection of
--remote-debugging-port is now insufficient, and the things left to watch are Special-APC injection into the browser's parent process, fake vtable structures in Chrome's address space, and unexpected local CDP WebSocket activity. The macOS analogue exists but a commenter reports it working on Intel only, with Apple Silicon still open.Where the AI belongs. “The Agent discovers, the Bot scales, and the Agent returns only for exceptions.” Rediscovering a solved problem on every run is not intelligence, it is an expensive way to reintroduce variance — and when the agent is pulled in mid-run, that intervention is itself the alert that the target changed. This is the escalation ladder the guide already documents, stated from the maintenance side: agent generation wins in the long tail of many changing sites, while a stable high-volume target still repays hand-written engineering. They were candid that their own browser-first approach “may be overkill” for a small workload on a stable API.
The two lines worth keeping verbatim. On cost: “We optimize for the cheapest method that keeps working, not the cheapest method that works once” — coverage, retries, freshness and maintenance set the real price, not cost per request, and the trigger for changing platforms is maintenance load, not raw scale. On self-healing: “AI can change how it reaches the same result, but it should not silently change what the Bot is supposed to collect.” Selectors, layout paths and loading strategy are safe to auto-repair; field selection, record scope and task intent are not, and a recovery that would alter what is collected is surfaced as evidence rather than persisted as a patch.
And the part that reframes trust. Agents adapt to page changes and broken flows; they cannot reason their way out of a burned IP, an inconsistent fingerprint or a session that no longer looks credible, because anti-bot scores the whole session, not the click. Their framing: the question has shifted from “can this bot act like a human?” to “does this session look trustworthy?” — which is exactly the layered, stateful trust model this guide's own detection map is built around. The one caveat: BrowserAct sells this platform, so read the architecture as a vendor describing its own product; the reason it earns a card is that the argument is independently arrived at and matches the measured behaviour elsewhere in the guide, not that the vendor made it.
The obfuscation side, same model. Castle's head of research shipped a vibe-coded VM-based JavaScript obfuscator as a side project and posted an obfuscated build of his own fingerprinting playground. Within hours a reader ran the same class of agent against it in a DM and reversed most of the VM handlers and bytecode, and the author's reaction — “I guess I know what I will do this weekend” — is the whole story: a hand-rolled VM protection did not survive first contact with an agent. This is the concrete, if anecdotal, instance of the REcon agentic-deobfuscation talk this guide already carries — the agent as an orchestrator of tooling that makes the debugging loop cheap — and it lands on the exact technique (VM/bytecode obfuscation) that anti-bot vendors lean on to hide their sensor scripts.
Why the two sit in one card. The reassuring result and the alarming one are the same capability seen from two sides. An agent driving a browser through screenshots is still weak at real-time motor tasks under a clock, so interaction defences that regenerate keep their value. An agent reading static code is now strong enough that a secret-in-the-client defence has a short shelf life. The load-bearing caveat is sample size: n=11 on one CAPTCHA lab and a single unquantified DM anecdote on the obfuscator. Neither is a benchmark; both are worth knowing precisely because they point the opposite way to the hype, in both directions.
blinker >= 1.8 removed a component the library relies on, so an unpinned environment that was fine last year breaks on the next resolve. The circulated working pin is selenium-wire==5.1.0, selenium==4.9.1, blinker==1.7.0 — treat that as a freeze, not a fix, because nothing upstream is coming.Why people keep it anyway, and the one feature worth keeping.
driver.wait_for_request() lets you block on the specific backend request you actually want instead of guessing a page-load timeout, then read its response directly — so you can skip rendering and parsing the whole DOM when the data you need is in one XHR. That is a genuinely good pattern, and it is the reason the library persists.The forward-looking read. If you are starting fresh, the same interception is available from maintained stacks — Playwright's request routing and response interception, or a mitmproxy sidecar — without inheriting an archived dependency and a hand-pinned
blinker. If you already depend on it, the pin buys time; put the migration on the list rather than discovering the breakage in a rebuild. This is the same lesson the guide drew from marking Scrapoxy discontinued: a scraper's most dangerous dependencies are the quiet, load-bearing ones nobody has looked at since they last worked.The two-header result is the one to act on. Of the sites that blocked Chromium-headless and nothing else, replacing
HeadlessChrome in the User-Agent and removing the HeadlessChrome brand from sec-ch-ua recovered about three quarters to a normal 2xx. That is two string edits against roughly eight points of measured penalty. It also lines up exactly with the independent proxy-benchmark finding this guide already carries, where DuckDuckGo split 95-of-95 against 0-of-50 purely on whether the User-Agent carried HeadlessChrome. Two unrelated measurements, same conclusion.Now put it next to what the vendors published the same week. A newsletter walkthrough had local headless Playwright failing 4 out of 4 against Cloudflare, Akamai, DataDome and Kasada, with the hosted product succeeding 4 out of 4 — a single run, on four of the hardest doors on the web, with affiliate links in the piece. A scraping API's post had bare
requests managing 1 of 5 URLs. Both baselines were unpatched clients on datacenter IPs. Bamberg's reading of that is unkind and hard to argue with: the baseline was failing partly for the reason that costs two header edits to remove, and the sample was drawn entirely from the tail. Neither vendor result is wrong; both measure the hard 10% and are presented as the web.The absence worth noticing. A 40,000-visit study whose entire purpose is attributing blocks does not discuss TLS, JA3 or JA4 anywhere. That is not evidence transport fingerprinting does not matter — the study did not vary it — but it does mean the layer this industry talks about most did not have to be invoked to explain the blocks it measured. Read alongside the barrier audit's own figure, TLS on 13.8% of landing pages against 78.1% co-occurrence on rate-limited sites, the shape is consistent: handshake work buys the tail, header hygiene buys the middle.
Honest limits. Landing pages, one visit each, no proxies, no interaction — so these are floors, and the pages you actually scrape sit behind the ones that were measured. The per-configuration split quoted above beyond the abstract's “15% versus 7%” comes from the full text rather than the abstract, and the paper predates the September defaults change.
Sep 2026 update. Every visit came from one Hetzner Cloud datacenter in Nuremberg, the VM rebuilt for a fresh IP each 1,000-site batch. The network never varied, so this paper cannot show that cloud IPs cause blocks; the authors say datacenter origin shifts absolute rates and leave multi-vantage work open. It is now being cited beside a home-versus-cloud agent test it did not run. What it does show on a fixed cloud IP: 772 sites blocked Chromium only headless, 14 only headed.
/goto link wrapper since August, described as “a custom encoding” that cannot be decoded locally. A researcher has now collected 324 unique tokens and published the structure, and three things in our own text need fixing.08 01), field 2 length-delimited (12). Every field-2 payload in all 324 samples begins with the same five bytes, 01 eb 3b 30 15, matching Google's Tink output-prefix convention of one version byte plus a four-byte key hint — so every token appears to be encrypted under a single key. Everything after that behaves like AEAD ciphertext: across 864 token pairs the mean same-position byte-collision rate was 0.003827 against 1/256 = 0.00390625 expected for random bytes. The cipher was not identified. Do not repeat this as AES-GCM. The obvious HPKE hypothesis was tested and rejected on canonical-encoding grounds: if the first 32 payload bytes were X25519 encapsulated keys their high bit would not be a coin flip, and across 324 samples it was — 178 zeros against 146 ones.Correction one: it is not just undecodable, it is non-deterministic. All 56 destinations that appeared across multiple independent SERPs produced a different token every time, plus 81 same-destination repeat groups inside single result pages. When the wrapper appeared, a large rank tracker asked publicly whether the mapping was static per page, because a static mapping means you build the lookup table once and amortise it. Nobody answered at the time. This answers it: no. That converts the measured 500-to-1,000 requests per five-page ranking from a startup cost into a permanent one, and it is the most decision-relevant fact in this sweep.
Correction two: HEAD is not blocked. Our text said it was, following the August reporting. The September measurement is that HEAD returns 200 with no
Location header while GET with redirects disabled returns the 302 and the destination. The operational advice is unchanged — use GET — but the failure mode matters, because a silent 200 reads as “my resolver is broken” rather than “that verb is refused,” and that difference is measured in debugging hours. Either Google changed behaviour between late August and early September or the earlier account was loose; both readings are on the record and we are not resolving it.Correction three, and the only local lever anyone found. The token is not fully opaque. Ciphertext length = destination URL length + 51 bytes for URLs under 128 bytes, or + 52 at 128 and above — 324 of 324, zero exceptions. So the token publishes the exact byte length of its destination, offline, free. Against a known URL universe that is candidate exclusion before you spend a request, and a cheap sanity check on a resolver. It is the only information the token gives up without a round trip.
What triggers it is the client, not the crawler. Chrome and Edge get
/goto; Brave and LibreWolf still get direct links; Safari gets the older /url wrapper with a CAES token inside it, so the token format is spreading beyond the new endpoint. Manually driven Chrome gets it too, so automation is not the trigger, and neither is geography — UK, Germany and Brazil exits all received it, and a solved CAPTCHA on a fresh EU exit still did. Saturation can be total: one run recorded 81 links, zero direct, 78 wrapped, 40 unique tokens — so deduplicate before resolving.Two numbers not to plan on. The published benchmark is 50 URLs on one machine, where five workers gave a 1.23s median against 3.27s sequential and ten workers were slower than five. Useful shape, moderate bounded concurrency over maximal — but nobody measured the sustained rate before Google rate-limits bulk resolution, which is the number that decides an architecture. And the claim that tokens are portable across machines and IPs and last for days is the load-bearing economic claim here, is unreplicated, has no measured TTL, and comes from a vendor selling the expensive half of that pipeline. Test the lifetime yourself before building a two-stage design on it.
The price, corrected. $0.40 per thousand is an image-to-text OCR rate, not a reCAPTCHA or Turnstile token. Checked against vendor pricing pages in September 2026, the real ranges are reCAPTCHA v2 at roughly $0.55 to $2.99 per 1,000 solves and Turnstile at roughly $0.80 to $2.89. No service we checked solves either for forty cents a thousand. Our own library table's long-standing figure of about $0.001 to $0.002 per solve — $1 to $2 per thousand — sits correctly inside that range, so quoting the map's headline would have put this guide in contradiction with itself.
And the axis does not fit two of its own entries. NopeCHA is a monthly subscription with daily caps, not a per-solve product: roughly $4.99 a month for 2,000 solves a day, $19.99 for 20,000, $49.99 for 80,000, $99.99 for 200,000. Saturate the mid tier and the effective rate is around three cents per thousand; run it lightly and you pay far above the map's headline. CapSkip is a flat licence in the $10–$20 a month range with an optional lifetime fee, explicitly marketed as having no per-solve charge. For both, cost is a utilisation question, not a unit price, and a table with a per-1,000 column silently misprices them in both directions.
Two things we are not repeating. The map places SolveCaptcha in the hybrid row; the vendor's own pricing page claims only automatic recognition and says nothing about human fallback, so that placement is the map asserting more than the vendor does. And the supporting claim that Arkose Labs' strongest challenges generate over 1,250 variations of a single problem is a real Arkose quote but a stale one — it belongs to the 2022–2024 marketing era and the current product page leads with risk-signal counts instead. It is a theoretical combinatorial space, not a measured solver miss rate, and this guide has Arkose on its own list of acknowledged blind spots. Leaving that gap open is more honest than filling it with retired marketing.
Whose map this is. The publisher sells proxies, and the argument attached to the map is that better IP quality means fewer CAPTCHAs arrive in the first place. That mechanism is real — reCAPTCHA v3 and Turnstile both take IP and ASN reputation as scoring inputs — but it is one input among fingerprint, behaviour and account history, and the vendor is not a neutral cartographer of a market it is arguing you should need less of. We could not locate the map's own page; the publisher's nearest public page uses three categories and places 2captcha in human-powered rather than hybrid, which is its own small warning about how firm any of these boundaries are.
SECURITY.md that declines to promise a security boundary between page content and the host — which is the correct disclosure for an engine that runs hostile JavaScript in-process, and is also the single most important sentence for anyone pointing it at the open web. Version v0.1.5 in May 2026 shipped fixes for a cluster of file-scheme advisories, credited to three outside reporters, after which local file access moved behind an explicit opt-in flag. The current release is v0.2.2, published 5 September 2026.The open one, with file and line. Issue #910, filed 8 September 2026 by a contributor and still open alongside its fix PR #911, reports that two WebCrypto ops size an output buffer straight from an untrusted
u32 with no upper bound, unlike the sibling PBKDF2 op which already caps at 1 MiB. Page JavaScript can drive either toward u32::MAX, forcing a multi-gigabyte allocation on the single-threaded runtime, pinning the V8 isolate and blocking every other CDP command on that connection, with OOM available. The detail that generalises is the mechanism: getRandomValues enforces the 65,536-byte WebCrypto limit in the JavaScript bootstrap, and a second JavaScript path — HMAC generateKey — feeds an attacker-controllable length straight past it into the uncapped native op. A guard written in the layer the attacker controls is not a guard. A second open issue reports layout and paint issuing direct, unproxied, synchronous requests that bypass proxy configuration, cookies and request interception, which is the same architecture failing in a different place — and for a scraper that one is a deanonymisation bug, not a crash.What we are deliberately not publishing. The researcher's post also claims a cross-origin read-and-write primitive via a caller-supplied frame id with no origin check, and arbitrary local file read through the ES module loader with a SyntaxError side channel that leaks a file's first line and its existence. We could not corroborate either. No advisory, issue, commit or changelog entry describes them; a merged pull request explicitly claims frames are published with security tokens for same-origin access while cross-origin frames stay opaque, which cuts directly against the frame claim; and the file-scheme class was already fixed and gated behind a flag four months ago. We also could not retrieve the original post to check its wording or whether it was coordinated disclosure. Fact-checking a paraphrase of a document nobody has read is not a basis for accusing a named 26.5k-star project of a disk-read vulnerability, so we are not doing it. If those two land as advisories, this card gets updated.
What to actually do. Nothing here says stop using Obscura, and its performance case is unchanged. It says treat it as an engine that runs untrusted code with a documented soft boundary: keep it off machines holding credentials you care about, do not run it with file access enabled, keep it patched — the current release does not yet contain the #910 fix — and remember that the reason it is fast is the same reason its page-to-host boundary is thin. That trade is the product, not a defect in it.
The guardrail was one sentence. The system prompt said refund amounts are capped at $100. That is the shape to recognise: a tool that spends money, guarded by a sentence. Moving the cap into the function — real programmatic enforcement — is obviously right and produced almost nothing on the score: two fewer failures, 1.89 points, still grade D. The reason is the sharpest methodological point in the piece. The judge grades what the agent said, not what it did. The tool call was blocked; the agent went on narrating refunds it had not made. In local mode there is no telemetry, so the judge reads the agent's own account of itself — meaning a harness like this can equally miss the inverse, an agent that quietly performs a harmful action while reporting compliance.
The finding that should change how anyone reads a security score. Run with one frontier model as the attacker and it declined to write the attacks, politely, over and over — producing an excellent-looking result that measured the attacker's refusals rather than the target's defences. Switching attacker models gave zero refusals across 488 prompts and the real numbers. Your agent's security grade is partly a property of which model you asked to attack it, which is the same failure this guide keeps meeting elsewhere: a benchmark that measures its own instrument. Costs are small enough that there is no excuse — three runs came to $2.88, a single multi-turn run at minimal depth $1.11.
Why this lands on a scraping guide, and what it undercuts. Agentic extraction means a model reading pages you do not control, which is prompt injection with extra steps. Another vendor post in the same week reported hardening against exactly that: fetched content arrives wrapped and marked untrusted with a per-response unguessable nonce, and they ran an injected page seven times with the agent ignoring the instruction every time. The nonce design is genuinely good — because the page cannot guess the closing marker, injected content cannot terminate the untrusted block early, which is a real anti-forgery property rather than a label — and the vendor is honest that it does nothing about a model that simply obeys the text inside the block. But seven single-turn trials is precisely the test that gave the support agent a B. The escalation attack does not appear until turn eight, and nobody is running eight-turn injection tests against their extraction agents.
The failure modes are all shaped like success. This is the part worth pinning to the wall. In the same tests: an HTTP 200 from Indeed and a 202 from Booking.com both carrying challenge markers in the body; a ten-URL batch call returning valid JSON for all ten items with an error document sitting inside one of them, in two runs out of three; and a 241-token empty wrapper that parsed perfectly and contained nothing. Every one of those passes a status-code check, a JSON-parse check and a “did the call succeed” check. Per-item content validation is not defensive programming here, it is the only thing that detects the failure at all — which is the same lesson this guide draws from a benchmark that had to distinguish an empty result from a refusal, and from every broken selector that returns a clean row with a null field.
One more, for anyone wiring an agent to a scraping tool over MCP. A form-filling browser tool in that stack publishes no MCP annotations, so it inherits the permissive default and can run unattended rather than landing in the write-and-destructive approval gate where it belongs. Check the annotations on every tool you hand an agent, because the absence of a declaration is not the same as a safe declaration.
What we are not repeating. The post's headline — an agent fetching five URLs itself managed one, and with the vendor's connector managed all of them — is a strawman baseline: bare
requests from a single datacenter IP with an unmodified Python user agent, which is exactly the client the independent 40,000-visit study says is cheap to fix. The one baseline success had no bot defences at all, and one of the five was refused by the vendor's own compliance review rather than by anti-bot. The token curve and the silent-success list stand on their own; the 1-of-5 does not. Cost figures in the piece are also stated by its author to be introductory rather than standing prices, so they are not planning numbers.The fix that is starting to appear is a scheduled re-measurement. A new Go client published this month runs a weekly job that installs current Chrome stable, re-measures it and opens a pull request when the profile has drifted, and it exposes the measurement as user-facing commands: one that captures a locally installed browser into a reusable profile, and one that runs the browser and the client against the same local echo server and diffs TLS, JA4, HTTP/2 settings, pseudo-header order and header casing. A diff against a real local browser is a stronger check than a matching JA4 hash, because the hash collapses exactly the fields most likely to have moved.
Two correctness traps in doing your own capture, both non-obvious and both worth stealing regardless of tool. A resumed TLS 1.3 session injects a
pre_shared_key extension, which changes the extension count and therefore the JA4 counters — so a profile captured on a warm connection silently describes a resumption while being replayed on cold ones, and a capture tool should refuse it as input. And measuring HTTP/1.1 and HTTP/2 needs two independent cold navigations, not one: a second navigation from the same page carries different Sec-Fetch-* values and contaminates the header profile.Keep the effort proportionate. The barrier audit puts TLS fingerprinting on 13.8% of landing pages — its lowest-prevalence barrier — but with 78.1% co-occurrence on rate-limited sites against 13.2% elsewhere. So handshake work is close to irrelevant on the broad web and close to mandatory on the aggressive tail, and the single most common mistake is spending a week on JA4 for a target whose refusal was a header string or an exit address. Note also that the independent 40,000-visit block study did not need to invoke TLS at all to explain what it measured.
On the new client specifically: it is Apache-2.0, Go with Node and Python SDKs over one transport, and it is days old — a first tagged release, one author, no external contributors, effectively no users. It is well built and it is not yet a dependency. Note also that its headline claim, profiles as measurements rather than hand-written guesses, is a real differentiator against the hand-derived lineage but not against the Rust proxy toolkit already in this guide, which also collects profiles from real devices automatically.
The part that matters if you were planning to fork it. You cannot. It was open source and then stopped being open source — the licence was restricted when the significant features landed and competitors began lifting the codebase into their own commercial services. The source will not be released, it remains the author’s property, and the current licence prohibits forking, redistribution and derivative works. The shutdown notice is equally unambiguous: “This is not a pause, a transition, or a handoff.” No community takeover, no new maintainer, no successor. Docker images are gone from public registries, public documentation is offline, and shared infrastructure is down for everyone except enterprise subscribers, who keep it until their contracts expire.
What actually exists today, from the same thread, because the question was a good one. ScrapeOps’ Proxy API Aggregator was named independently by four practitioners including two proxy-company founders — a hosted endpoint that routes across providers. Kadoa said they built their own; so did several others; one commenter put it flatly: “Most companies build their own version of this.” And the sharpest note in the thread is the sceptical one — that the routing logic is a few days of work over a small table of per-domain success rates per provider, and the open question about any hosted aggregator is the surcharge. Which is precisely the economics Scrapoxy hit from the other side.
Correction to this guide. Our library table listed Scrapoxy as a live proxy manager and linked to
fabienvauchelles/scrapoxy — a one-star mirror whose last commit was December 2024 and which now says only that the project moved. Both are fixed: the row is marked discontinued, the link points at the end-of-life notice, and the “free open-source alternative” line is gone, because it stopped being true before the project did.The clause that should change how you present yourself. Mixed crawlers — ones that do not give the site owner the ability to distinguish between search, agent use and training — are blocked on all pages with ads. The three-way split is not merely a taxonomy for Cloudflare’s convenience; refusing to sit in one of the boxes is itself the failing category. An undeclared general-purpose crawler does not fall back to the permissive Search default. It lands in the blocked bucket everywhere the condition applies.
Why the ads condition is the interesting engineering detail. The gate is not “is this a bot” and not “is this content valuable” — it is whether the page carries a monetisation model that the automated read bypasses. That is a per-page property, not a per-site one, so the same origin can be open on its documentation and closed on its articles, and a crawl that succeeded across a site last week can start failing on exactly the subset that pays for the site. If your monitoring aggregates success rate per domain, this change is invisible to it until coverage has already fallen.
What to do before the date. Take a baseline now, per URL class rather than per domain, so a 16 September drop is attributable rather than mysterious. Where you have a relationship with the publisher, this is the cheap week to have the conversation. And where you run an agent on a person’s live behalf, note that Cloudflare has given that its own category, distinct from training — the first widely-deployed default that treats “a bot acting for a human right now” as its own thing to allow or deny, rather than lumping it in with scraping.
Sep 2026 update. Cloudflare revised this on 15 September itself. Earlier Training blocks migrate to a new Disallow AI Training setting that writes robots.txt and keeps Accountable mixed-use crawlers (Googlebot, Bingbot, Applebot) allowed for search; only an explicit Block, or Block on pages with ads, now stops them. Never-configured domains migrate to Allow on all three, and new ad-supported domains are offered Disallow for Training and Block on pages with ads for Agent.
Oct 2026 update. The three-category framing now circulating is right, with two precisions from Cloudflare’s 15 September post. A new domain without ads defaults to Allow on Search, Training and Agent; only ad-supported ones get Disallow AI Training and Block on pages with ads for Agent. And Disallow is a robots.txt signal plus a block on non-Accountable training crawlers, while Agent has no Disallow option yet, so an agent on an ad page is refused at the edge.
navigator.cpuPerformance as a coherence check — a coarse 1-to-4 hardware tier, crossed against hardwareConcurrency, looking for a machine telling two stories about itself. A researcher has now published the actual implementation and a working prototype, and it is considerably worse for a spoofed browser than “the two should agree.” The base table is a pure function of thread count: 1–2 → 1, 3–4 → 2, 5–12 → 3, 13+ → 4. Computable exactly, by anyone, for free.So the exception list leaks. Each carve-out is a set of chips, and a few of the resulting pairs are nearly singletons. Ten logical processors reporting tier 4 is Meteor Lake-U — a Core Ultra 5 115U, 2 P-cores plus 4 E plus 2 LP-E giving 8 cores and 10 threads. That is one SKU. Eight and tier 4 is Lunar Lake (the whole Core Ultra 200V family caps at 8 threads, the Core Ultra 9 288V included) or an Apple M, and the WebGL unmasked renderer separates those two in one further read. And nine logical processors on a Core Ultra is a count that does not exist on bare metal — no part ships it — so it points at a VM or a cgroup-limited container on a Core Ultra host. A virtualisation tell, arriving free, from an API nobody thinks of as one.
The position for a scraper is unchanged but sharper. Spoof one hardware property and you now own all of them, and this is one you cannot fudge plausibly: the expected value is computable, the exceptions are enumerable, and the fleet where these stop agreeing is precisely a headless fleet on undersized cloud instances. The prototype is worth running against your own stack rather than reasoning about.
Two honest limits. The API needs a secure context and Chromium 152 or newer. We opened the prototype from Chrome 148 and it correctly reported the tier absent and the test inconclusive — so this is a signal arriving, not one already deployed everywhere. And the author says plainly that the exception list can move between Chrome versions, so anything built on it is a maintenance commitment rather than a constant.
Oct 2026 update. Chrome 152 reached stable on 25 Aug 2026, so the tier is now deployed, and users can override it under Settings > Performance (admins via
CpuPerformanceTierOverride), so an odd tier can be a setting. A Web Scraping Club explainer (6 Sep) adds the RAM neighbour, confirmed in Chrome 147’s notes: deviceMemory now reports 2–32 GiB on desktop and 1–8 on Android. A desktop Chrome 147+ reporting 0.5 or 1 is incoherent.Timezone and locale alignment did not pay off. Flat on Patchright, 34% against 35% — and zendriver lost six sevenths of its yield, 57% down to 9%, p = 0.0008. Two engines is a thin basis for a rule and they say so. But it is worth reading against the post’s own comment thread, where two practitioners independently offered timezone mismatch as the explanation for the headline result. The measured arm says the opposite. Neither side has enough to settle it, which is the actual state of knowledge and better than the confident version of it.
The methodology is the transferable part, and most of it is about not fooling yourself. Verdicts come from page content, never HTTP status — the same Google reCAPTCHA page arrived once as 429 and once as 200.
empty is not block: Google hands a scriptless client a 92 KB “enable JavaScript” scaffold that stays on /search and refuses nothing, and 14 of 14 such rows carried the enable-JS marker while none carried reCAPTCHA — score that as a block and you credit Google with a refusal it never made. error is yours: a harness counting its own crashes as target refusals manufactures a convincing result while measuring almost nothing. A refusal also diverts rather than announcing itself — Google answers by sending you to /sorry/, with a 200 about a quarter of the time — so the real test is whether the final URL kept the host and path you asked for.Two findings about their own instrument, published rather than quietly fixed. Their harness was getting its own pool banned: one unauthenticated CONNECT per session, sent by the browser before anything else, tripped an IP ban that read as a gateway floor for days. And until 11 August, Camoufox alone opened a fresh context per query and discarded its cookie jar while every other engine carried one — a session-continuity bug that had quietly been making it a different experiment. Five of the ten findings replaced an earlier claim of theirs, and both versions stay in the notebook. The Amazon one reversed outright: on a workstation in early August, Camoufox was served 90% while every Chromium engine met the throttle, which read as a Firefox-beats-Chromium result; on the server the unmodified control came out on top and the Firefox reading was simply gone.
Three operational notes worth stealing. Response bodies are kept gzipped, and re-reading 250 stored Amazon bodies offline later found an Akamai interstitial and an AWS WAF challenge that had been filed as refusals — 21 historical rows corrected at no traffic cost. Headful on a headless host needs
xvfb, but a virtual display does not restore the GPU, so WebGL falls back to software and a headful server run is not a headful workstation run. And Playwright, Patchright, rebrowser and cloakbrowser pin four different Chromium builds sharing no download — on a stack where several findings turn on the build, that is a variable most comparisons never mention holding.timeoutMs is a hard kill rather than a cooperative signal, so a busy-looping artifact cannot run forever. Credentials are declared as named inputs and injected at run time, never embedded in the artifact and scrubbed from anything sent to the model. There is a noSandbox escape hatch, and the documentation tells you to use it only on artifacts you trust like your own code — the correct framing for something a model wrote.The generation loop refuses to take the model’s word for facts it can check itself. The scout drives a real browser on condensed reads — links and text, never raw HTML or the accessibility tree — verifies selectors, detects listing patterns, exhausts pagination, and runs a no-JS probe that decides cheerio versus Playwright. Its exit tool is gated: a report with unverified selectors, invalid sample items or an unproven cheerio claim is rejected with instructions and the model corrects itself mid-run. At codegen the library then overrides anything it verified itself — the engine choice, the selector samples, the assertion floors. Self-test lints statically first, so obviously broken code never touches the site, then runs live with every item validated and assertions checked, repairing up to three times. You only ever receive an artifact that has passed.
Politeness is the default rather than a flag. robots.txt per origin with disallowed URLs refused, Crawl-delay honoured, 500ms between requests, 100 pages and 10 minutes per run, and a User-Agent naming the project and linking its repository. All of it configurable, with the docs noting that what you do with the knobs is on you. Cloudflare gets its own retry schedule — a 403 or 503 carrying
cf-ray or cf-mitigated markers waits 5s doubling to a 300s cap over 4 attempts, outside the ordinary retry budget — and those waits count against the ten-minute ceiling, which is exactly the sort of interaction that bites in production.What we are not claiming. It is five days old: v0.3.3, 19 commits, zero stars, zero dependents, MIT, from a maintainer with a long open-source history. The $1,000-a-month figure is one project’s bill, not a benchmark, and there is no published comparison against keeping a model in the loop. And the tool warns about itself in a way worth repeating: generation performs up to
1 + maxRepairAttempts real crawls of the target while self-testing, so develop against a fixture or a staging copy unless you want the site to meet your prototyping.rama serve proxy --mitm, a protected inspector showing connections, requests and responses live, each replayable and exportable as HAR or cURL — and, the reason it belongs in this guide, exportable as a User Agent HTTP/TLS profile.The rest of it. Protocols layered on HTTP get specific handling — WebSocket messages are visible and replayable in both directions and you can inject your own, which is the part most HTTP debuggers do badly. There is ICAP support, meaning an external service can inspect and modify traffic out of process:
--icap <URI> points the proxy at your server, and rama serve icap gives you an echo service to point yours at, so scanning or rewriting happens somewhere isolated rather than inside the proxy. Being MITM, the client has to trust the CA, which the inspector will hand you or --mitm-ca-cert writes to disk up front.Status, stated plainly. Announced 28 August 2026 for the 0.5 release, expected around the end of September. The MITM capability landed in the repository last month; the release has not shipped. Treat this as something to try when 0.5 lands, not something to plan a pipeline around this week. Worth knowing separately that the project already runs public diagnostic endpoints — an echo service, IPv4 and IPv6 reflectors, and a fingerprinting service that doubles as the collector for its own embedded user-agent profiles.
Compliance is tightening in the same direction. Bright Data now requires KYC universally for residential proxies rather than only for specific destination sites. Read alongside the seizure, the trend is a single thing: the residential market is being pushed from “buy access to a pool” toward “be identifiable to the person selling you the pool.” If your procurement assumes anonymous purchase, that assumption has a shelf life.
And the money went the other way entirely. Nimble raised a $47M Series B in February 2026. Oxylabs took $130M — the guide already noted the round and the reported $3.6B valuation; the investor is Warburg Pincus. Set against a regulator seizing a NASDAQ-listed competitor’s domains in the same year, that is the clearest available signal that capital reads the enforcement risk as a consolidation event rather than an existential one: the compliant survivors get more expensive and better funded, and the cheap end of the market is the part being removed.
The result everyone will quote, with the condition attached. Given the generic prompt — here is an obfuscated function, deobfuscate it and reconstruct the high-level code — the agent produced nothing usable after several hours against a commercial VM obfuscator. It only worked once it was handed a procedure: the author's own 2021 write-up on writing disassemblers for VM-based obfuscators as its knowledge base, the script templates from that post, and the three tools. Then it ran one to two hours autonomously against what the speakers estimate as several weeks of full-time manual work. The self-referential detail is the honest one: the agent's ceiling was an existing human explanation of the problem.
The pipeline, because it transfers. Emulate entry to exit with unicorn and treat that as ground truth, self-repairing failures as they appear. Re-run the same path under symbolic execution, checkpointing against the emulated trace at every step so the agent knows it is still on the real path. Lift each handler's semantics from the validated trace. Extend the script into a full disassembler. Reconstruct. Against a kernel anti-cheat the agent found one shared interpreter across roughly a thousand VM entries, wrote a single disassembler, and devirtualised them in parallel.
The failures are the useful half. A recent game DRM defeated it outright: meaning is runtime-bound, so it stalls under emulation. A deflattening pass written for one malware family did not work at all on the next and had to be rewritten, then still failed on the MBA opaque predicates. On call obfuscation the agent tried cryptographic identification of a jump table and then brute-forcing every address in the binary before a human stepped in. The warning to carry: each pass stays sample-specific. There are no benchmarks, no success rates and no cost figures anywhere in the talk.
What they say impedes an agent. Four dimensions, and this is one slide plus a Q&A answer rather than a measured result, so weight it accordingly. Budget and search traps (state expansion, costly oracle checks) attack the token and tool-call budget. Protection stacks bind meaning to state unavailable at analysis time — control-flow-sensitive rolling-key bytecode where each handler derives the key decrypting the next, so nothing can be enumerated or skipped. Tool friction got the sharpest answer: anti-emulation mostly means anti-QEMU, finding behaviours the emulator models differently from real hardware and gating a loop on them so it spins forever — and the argument is quantity, "the agent gets stuck and stuck and stuck", because each divergence costs a full debug cycle and the agent cannot know how many remain. Narrative poisoning: fake symbols, planted strings, prompt injections aimed at the model rather than its tools. Asserted, not demonstrated.
What transfers to JavaScript, and what does not. The VM workflow maps one to one onto bytecode VMs in commercial anti-bot scripts, and rolling-key handler decryption is the same primitive doing the same job. "Each pass stays sample-specific" is exactly the Babel transform that works on one bundle and dies on the next. Narrative poisoning is more potent in JavaScript, because an agent reads real source text rather than decompiled pseudocode, so planted strings and injections land directly in its context — strip strings before feeding a challenge script to an agent. But the tool-friction defence is much weaker here: it depends on the emulator being an imperfect model of hardware, and in JavaScript you simply run a real browser, so ground truth is nearly free and there is no fidelity gap to exploit. There is no lifting problem either, since a parser hands you exact function boundaries for nothing. And the threat model is inverted. The talk is about protecting code you ship; anti-bot is about classifying the client. An agent that fully deobfuscates a sensor script still has to emit a payload that survives server-side correlation — which is precisely why this industry put its real defence there.
What is theirs and what is borrowed. Their own YOLOv8 is a UI detector (five classes: captcha area, cell, checkbox, reload, submit; mAP@0.5 0.995, ~1.1ms/image). The tile classifier is Plesner et al.'s publicly released YOLO, reused as-is. The paper says so: "Because both systems use the same published YOLO classifier, these gains come from the hybrid design rather than a stronger detector." The VLM is Qwen-7B-VL, self-hosted, routed at a confidence threshold of 0.70; YOLO handles ~70% of cells at 4ms, the VLM is called on 37.1% of puzzles at 225ms to 2251ms.
The comparison is weaker than it reads. The 2 → 5 → 7 → 13 ablation (median challenges per CAPTCHA as cookies, Bezier curves and mouse simulation are stripped) and "blocked entirely after 20 runs" are Plesner's published numbers restated, not re-measured. And Plesner's solver skips puzzles whose class it does not support (Boat, Taxi, Tractor, Stairs), so the 100%-versus-100% is not like for like.
"Learns from every encounter" is offline, not online. No tile cache, no live gradient updates. It is teacher-student distillation between runs: the VLM hard-labels new classes, YOLO is fine-tuned with Experience Replay. Honest about the cost — Taxi reaches 96% recall after roughly two puzzles, Boat plateaus near 48% using every training image, and catastrophic forgetting runs 6.8 to 17.2 points on the existing 13 classes. Labels are used unfiltered, and VLM labelling accuracy was 82.1% on Taxi.
The genuinely interesting result is adversarial. PGD perturbation drives Plesner's YOLO to 0.0% accuracy; the VLM only drops 3.7 points. So the VLM relabels, YOLO retrains, and the loop recovers across a simulated twelve-month arms race. Their explanation is the part worth keeping: a cheap, roughly 70%-accurate open-weight teacher hardens the student as effectively as a perfect oracle, because noisy labels decorrelate the solver from the defender's surrogate model, so crafted perturbations stop transferring.
The practitioner counter, from the post's own comments. A demo page is mostly grading the token, not the pointer. The layer that survives losing CDP and navigator tells is kinematics: OS-level synthetic events arrive without the HID jitter and sub-pixel noise a real mouse produces, often land on exact integer coordinates, and on Windows carry an injected flag the page cannot read but the endpoint can. Cite this paper as "on Google's demo page", never as "against reCAPTCHA v2".
Three of the four winners rebuild the browser rather than patch it. No HeadlessChrome string in the UA, nothing reading as CDP automation. That is the finding, and it matches this guide's engine-level thread: flags applied on top of normal Chrome leave a seam that a detector reading twenty signals will find.
Read the scope before you buy anything on it. The benchmark deliberately covers the JavaScript fingerprint surface only. Transport (TLS and HTTP/2), request timing and proxy exit are explicitly excluded, on the stated reasoning that swapping a Python library cannot change them. So a 100 here means "clean on the JS surface in headless mode on an M2", not "will get you in". Every Chrome-based tool was driven with channel="chrome" against real installed Chrome, precisely so bundled Chromium's SwiftShader renderer would not be miscounted as a tool failure — a methodological care worth copying. Probe score and detector verdict agreed on 12 of the 13 tools where a verdict was captured.
Date discipline. The article carries a publication date of 23 July 2026 and resurfaced on social in September. Treat the table as a July measurement of fast-moving tools, and re-run rather than cite it in December. The harness is in the author's subscriber repository; the results table and heatmap are public, the per-tool analysis is paywalled.
Why "tied or beat" is the only honest verb. Two-sided Fisher exact against the leading cell with a Bonferroni threshold of 0.0071: stock chromium 96% (419/436), rebrowser 95%, botasaurus 94%, camoufox 92%, zendriver 92%, seleniumbase 92% are all statistically indistinguishable. Only cloak at 80% and patchright at 63% (288/457) separate, and they separate downward. Patchright sits 33 points below the engine doing nothing at all. Two tiers, not eight places.
Findings worth more than the headline. Chrome burns 43MB per fresh profile talking to Google before you request anything — 43.2MB of a 43.4MB idle window to optimizationguide-pa.googleapis.com on about:blank, which is roughly 43GB per thousand attempts billed as residential traffic. The host outweighs the proxy on Google: same code, same gateway, same target, 39% (24/61) on a Windows workstation against 0% (0/84) on a Linux VPS in overlapping hours, Fisher p = 3.7e-11. DuckDuckGo reads the User-Agent and nothing else: 95/95 pass for engines whose UA omits HeadlessChrome, 0/50 for the two that carry it, across three browser families. The TLS handshake was not the discriminator: chromium, patchright and obscura emit byte-identical ClientHellos yet finish 44 points apart — and compare JA4, not JA3, because Chrome shuffles extension order per connection.
The method choices to steal. Verdicts come from page content, never HTTP status, across ok / captcha / consent / block / empty / error, and empty is not block — Google's 92KB "enable JavaScript" scaffold rejects nothing, and 14 of 14 such rows carried enablejs and none carried recaptcha. Harness errors are excluded from the denominator instead of counted as failures. Cells interleave at batch granularity because "the hour is the largest confound this repository has found: the same gateway, country and browser moved 69 points to 52 between two windows of one afternoon." A matrix that cannot be controlled is refused by a dry run rather than silently adjusted. And the unmodified control is inside the matrix, enforced by a test that reads the source and fails the build if browser arguments or a UA override ever appear.
Standing caveat. The author is a proxy vendor. The repository asserts that nothing in it is a sales number, which cannot be audited without re-running the harness — which is, to its credit, exactly what it is built to let you do.
His test for a vendor. "Can it reliably tell that the client hitting your application is automated in the first place?" He wants credential stuffing blocked, fake account creation with disposable domains caught, and measurable counts of fraudulent accounts created — over intent classification. "These problems are much less fashionable than AI agents, but they are also much easier to measure." Note honestly that his piece names no numbers on bypass release frequency; the acceleration claim is his observation from Discord, Telegram, Reddit and GitHub, not a dataset.
The comment thread is where the useful engineering rule appeared. Asked why anti-detect browsers are the default when generating payloads directly is cheaper for scraper and target alike, he gave a decision rule worth writing down. Scraping as a service, across many sites and many anti-bot vendors and versions: invest in the anti-detect browser, proxies and realistic mouse movement, because maintaining one solver per vendor per version is not sustainable. A small number of target sites at very high volume — his example was millions of profiles continuously — and the non-browser client scales better. That is the same escalation ladder this guide teaches, priced by breadth against depth.
Two refinements from other practitioners in the thread. A newsletter author argued the binary is gone: bot or human is now bot, human, or bot acting on behalf of a human, and no vendor can comfortably sell "we block AI agents too" to a customer who reads that as lost revenue. A detection engineer noted the malware field already has vocabulary for this, pointing at Recorded Future's AI Malware Maturity Model, which sorts AI-assisted development from genuinely AI-driven execution — swap "malware" for "bot" and it mostly transfers. A reverse engineer pushed back on the acceleration itself: against VM-based commercial protections, AI alone is not enough, and the author agreed — it is the combination of a skilled reverser and AI, not AI on its own.
Where it stops, from reading the source. The array must be a top-level variable. Stock obfuscator.io hides it inside a self-rewriting getter function, and the extractor never descends into function bodies, so it raises "No string array found" on the most common real-world input. Decoder calls must have exactly one numeric-literal argument; obfuscator.io's decoder is two-argument, and those call sites are silently skipped. There is no alias resolution, so the routine practice of assigning the decoder to a second identifier defeats it. Dead-branch removal fires only on literal true and false, not on the customary negated-array idioms. And the sandbox has no window, document or navigator, so any self-defending, domain-locked or environment-probing decoder throws.
Provenance worth checking before you adopt it. The repository has zero stars, one commit dated 20 June 2026, and nothing pushed since; the September announcement is a re-post of unchanged June code. Its own test suite is four assertions against a single hand-written 944-byte fixture that approximates the shape rather than reproducing real obfuscator.io output. There is no comparative benchmark, and the README does not mention webcrack, synchrony, restringer or de4js — all of which already handle the getter-function array, two-argument decoders, alias resolution and control-flow unflattening.
Why it is still worth reading. The companion write-up shows the technique working at real scale against a live anti-bot challenge — hooking Function to capture a 350KB evaluated body, then running it across a dozen payload generations. That harness, the Function-hook loader interception and the faked browser environment, is not in the repository. What shipped is the vendor-neutral skeleton. Take the stance, which is correct for per-request-regenerated payloads, and keep webcrack for everything else.
The connection-pool trap is the one that bites scrapers. Go's default MaxIdleConnsPerHost is 2. Past worker three, nearly every operation pays a fresh TCP and TLS handshake. At two thousand workers against one host that is a full round trip added to every request, and it presents as mysterious latency rather than as a configuration error. Set MaxIdleConns, MaxIdleConnsPerHost and MaxConnsPerHost to the worker count and IdleConnTimeout to 90 seconds.
Backpressure is one buffered channel. When it fills, the producer blocks and the system slows intake instead of growing the heap. Queue size sets buffering tolerance; worker count sets sustainable rate. They are separate dials and conflating them is how fleets die.
Measurement discipline. "Throughput and tail latency only mean something together, and only when you know which stage produced them." Their own mock p99 is called out as unreliable, capped by simulated latency plus jitter rather than reflecting DNS, TLS and retry tails. And the memory arithmetic is the kind nobody does until it hurts: eight-byte latency samples at 8,000 per second accumulate 62.5KB per second, 230MB per hour, 5.5GB per day, and the harness sorts the whole history on every report call, doubling it. Production wants a fixed-size bucketed histogram rotated per window.
Credit where due. The article answers its own headline honestly. Asked whether the sample command solves 8,000 real CAPTCHAs per second: "No." The live sample runs about four per second, network-bound, on twelve requests through six workers. The 8,000 is an architecture target across distributed workers, and the piece says so rather than letting the number sit there.
Sep 2026 update. Hosted browser fleets ship this bounded queue as product behaviour. A Browserless blog post (vendor, about its own service) says bursts up to twice your concurrency limit wait their turn and anything beyond returns HTTP 429. Its self-hosted docs confirm the shape, a configurable
QUEUED cap then 429, but not the 2× ratio. Treat that 429 as backpressure to slow intake, not as a block to retry through.Oct 2026 update. A practitioner checklist (no measurements) maps symptoms to the stage at fault. Low CPU with stuck jobs means waiting on I/O, locks or a pool: check task states and timeouts. More workers making things slower means saturation: check queue depth, rate limits and database load. A worker that vanishes is usually memory: read the exit reason. Records lost after a crash point to uncommitted buffers: commit and checkpoint per batch.
The correction that matters most for practice. The hardest failure point for these engines is not canvas or GPU randomisation, it is the TLS handshake — which is exactly what the independent harness found when chromium, patchright and this engine emitted byte-identical ClientHellos and still finished 44 points apart. Per-session randomisation of GPU, canvas, audio and battery is the axis being advertised, and it is not the axis doing the rejecting.
This is consistent with what this guide already recorded. Our own teardown against a hardened commercial target concluded the wall was the IP, not the fingerprint, and the engine's own release notes warn that rendering and Web API behaviour can differ from Chrome. The tool is genuinely interesting for its actual purpose — a cheap, fast, machine-consumer browser for agent workloads, where the rendering pipeline is dead weight. It is not an evasion product, and buying it as one will cost a week.
The general lesson, again. A star count is not a measurement, a benchmark on the author's own page is not a benchmark, and "detectors can't catch it" is a claim with a date attached whether or not the poster writes the date down.
His argument to defenders. Solutions that update monthly or less often cannot track threat actors who iterate continuously, so buyers should assess protection vendors themselves rather than defaulting to whichever option is easiest to justify in procurement. "Short-term peace of mind is exactly that: short-term. It does not protect a business model built on unique intellectual property."
Read alongside the vendor-drift argument elsewhere in this feed. One side says the anti-bot industry is chasing agent governance while classical bypasses ship; this one says the buyers are choosing on procurement convenience while the bypass market publishes a rate card. Both describe the same gap from different ends, and both are written by people who sell protection, which is worth holding in mind while agreeing with them.
What is actually cached matters. Not page content, and not embeddings. The structure — where the fields live. That is why it stays current: the map is stale only if the layout changes, while the values are read live on every visit. Cache the content and you have a freshness problem; cache the structure and you have a drift problem, and drift is the one this guide already knows how to detect.
The honest caveat. These are a vendor's own comparative numbers against named competitors, not an independent benchmark, so treat the ratio as the claim rather than the fact. But the shape is not in dispute, and it is the argument for the pattern: paginated listings and linked detail pages are where one learned template pays for itself hundreds of times over. The expensive question is not what a page costs. It is how many times you pay to understand the same page.
tls-client lets you construct that handshake to match a real Chrome release byte for byte. Python's standard ssl module does not expose that layer at all — you get whatever OpenSSL negotiates, and it does not look like a browser. That is why the Python ecosystem reaches for curl_cffi, which is a binding to a patched C library rather than something the language offers.The concurrency half. Roughly 2KB of stack per goroutine against about 8MB per OS thread, with Go scheduling tens of thousands of them across a handful of threads. A Python thread pool of fifty is already substantial; the orders of magnitude are not close.
The part worth stealing regardless of language. They run a multi-armed bandit that learns which TLS profile actually works per site, using Thompson sampling — the same method this guide already documents for choosing between live and dead proxies, applied one layer down. Which Chrome profile to impersonate is exactly the kind of decision that is per-target, drifts over time, and is usually hard-coded once and never revisited. It is not a global setting. It is a per-domain hypothesis you should be updating.
Oct 2026 update. An industry datapoint for the systems-language side: an October 2026 job advert from Lighthouse (formerly OTA Insight, hotel rate intelligence) describes its crawler team building “mostly in Rust” systems that handle close to 200 million requests a day, about 2,300 a second averaged, and asks for curiosity about “what really happens on the wire”. The caveat: a hiring post’s self-description; no architecture, benchmark or engineering write-up from Lighthouse was found.
google.com/goto?url=… with an encrypted token, resolving through a 302. Ordinary searchers notice nothing — one extra hop, same destination, no interstitial. Anything that reads a results page programmatically is hit hard. By late August it was near-total across several residential providers.Location header rather than a refusal, which is the same advice with a much more confusing failure mode.It compounds, it does not replace. The
num=100 removal in September 2025 multiplied the number of pages needed and raised operating cost roughly tenfold. This multiplies the requests per page. In Perkins' framing the two changes compound rather than substitute.The design insight, and it is the good one. As one bot-detection researcher put it, this is a speed bump rather than a wall. Client-side detection can be argued with. This cannot: nobody clicks every result on a page, so a whole /24 doing exactly that is self-identifying. You either self-moderate your click-through and lose coverage, or you look like a subnet that reads a hundred links a second. Either way Google gets what it wants.
And it may not be a rollout at all. One reading circulating among practitioners is that the wrapper is what Google serves to clients it has already classified — which would make its appearance a detection signal about you, not a product change. Google has published no documentation, no rate limits and no permanence timeline, saying only that it deploys technical measures against evolving abuse.
Cache-Control: immutable, so a returning human fetches only the HTML, which is indistinguishable from an HTML-only scraper.The line worth keeping. On JA3/JA4 they noted that
curl-impersonate clones Chrome's handshake exactly, and drew the conclusion most vendors will not say out loud: the better the impersonation, the more the attacker's fingerprint converges on the one your real users have. Fingerprint blocking is safest while your attacker is incompetent, and most destructive the moment they improve. A detection method whose false-positive rate rises with attacker skill is a liability disguised as a control.Every finding is said to carry its run id, its denominator and the date it was collected, which is exactly the discipline this guide has been asking for from vendor numbers.
Read the Amazon result carefully, because it is a claim about a target, not about the tools. It says Amazon's defence on that path was not keyed to the signals anti-detect engines fix, and that adding one bought nothing there. It does not generalise to Cloudflare or Kasada. The honest takeaway is the one this guide keeps arriving at: measure on your own target before you buy a stealth layer, because the layer that matters is target-specific and the default assumption is frequently wrong. Reported by the harness's author; the repository is days old and not yet independently reproduced.
/api/products uses an access_token, it identifies the request that produced that token, and accounts for later mutations of it.What it handles: values that are URL-encoded, Base64, base64url or carried as Bearer tokens; recursive dependency chains; values that change over time; and reduction of a large capture down to just the requests one operation actually needs.
The discipline worth copying even if you never run the tool. It keeps observed evidence separate from unresolved hypotheses, and explicitly refuses to treat correlation as proof. That is the same distinction this guide draws about decoded string tables versus values a program actually reads, and about a fingerprinting inventory built from grep. A dependency you inferred from proximity is a guess; a dependency you traced by value is a finding. Early-stage alpha, described here from its author's own write-up.
What actually breaks first, in rough order: HTTP connection pool limits, which cap in-flight work well below what your worker count suggests; unbounded queues, which convert a temporary slowdown into a memory exhaustion; and per-connection memory at sustained volume, which is fine in a burst test and fatal over hours.
The controls. Bounded queues with real backpressure, so a slow downstream slows the producer instead of filling RAM. Worker pools with explicit concurrency limits rather than unbounded goroutines or tasks. And measurement at p50 and p99, not the mean — because at 16,000 in flight the tail is what determines whether the queue drains or grows without bound.
Why a stale table is worse than stale prose. A number in a comparison table gets read out by assistants, pasted into decisions and quoted back months later with the date stripped off. Prose carries hedges; a table cell does not. If you publish a benchmark, publish the build it ran against, because the tool underneath a pass rate can change without the pass rate changing.
The general rule, which this guide will now follow. Any performance claim carries three things or it does not go in: the date, the exact build or version, and the targets it was measured against. A pass rate without a base version is a rumour with a percentage attached.
What actually remains, honestly. Real devices, which means a physical handset farm with the cost and operational drag that implies. Harvesting tokens at low rate from real devices and spending them carefully, which changes your architecture from throughput to budget. Or the answer nobody wants: the endpoint is closed, and the web surface you were trying to avoid is the surface you have.
The practical read. Check for attestation before you invest a week in reversing. If the app calls Play Integrity or App Attest on the endpoint you want, the reversing effort has a known ceiling and you should price the alternatives on day one rather than day nine.
robots.txt, ai.txt, CAPTCHAs and login walls as evidence about what data subjects could reasonably expect. Where a site signals objection through any of them and is scraped anyway, individuals are less likely to have expected the processing, and the legitimate-interest balancing test gets correspondingly harder to pass. A technical measure you bypassed still counts against you, because its legal function is to express an objection rather than to stop you.The test itself is three cumulative conditions under Article 6(1)(f): an interest that is lawful, clearly articulated and real rather than speculative; necessity, meaning no equally effective and less intrusive route exists; and a balance where the data subject's rights do not override it. All three, or the basis fails.
Who inherits this. Anyone selling scraped data into an EU-market model provider, and anyone buying an already-scraped dataset from a broker — the guidelines cover obtaining as well as collecting. Draft guidance in consultation, not settled law, and not legal advice.
And the case it expressly reserved. The panel left open the opposite configuration — an agent with more autonomy, or one communicating server-to-server with the target. So the local-agent versus cloud-browser choice this guide already frames as an architecture and cost decision now carries different exposure on each side of the line, and the cloud side is the one the court did not bless.
Two details squarely on this guide's territory. The dispute began partly because Perplexity would not send an identifying user-agent string, and the court held that refusal irrelevant to whether access occurred. And the opinion preserves the contract route, which means the practical lever moves back to terms of service and technical blocking rather than the computer-crime statute. First circuit to rule on this. Not legal advice.
A concrete artifact to start from. AliExpress serves
collina.js and fireyejs.js from its AWSC path, and they build a WebAudio graph that stays running — a sawtooth oscillator into an analyser into a script processor into a gain node pinned at zero, terminating at the audio destination. Not the one-shot sample this guide's existing AudioContext material describes, but a persistent pipeline. It is conspicuous enough that it has been observed holding Bluetooth multipoint connections open.What that changes about technique. A persistent audio graph is a different detection surface from a one-shot fingerprint: it can observe over time, and it survives page interaction. Anything you learned about spoofing a single AudioContext read does not transfer.
The engineering nobody writes down. Real feeds are snapshot-plus-delta: you get a full state once, then increments. Miss a delta and every subsequent value is wrong while remaining perfectly well-formed — the exact silent-corruption shape this guide keeps warning about, arriving through a different door. You need sequence-number gap detection and a resync path, not a retry.
The detection posture differs too. Servers check
Origin and the Sec-WebSocket-* headers at the handshake, so the entire fight happens once at connection time rather than per request. That cuts both ways: fewer chances to be caught, and a much longer-lived session to be judged on./api/mcp on the storefront domain, with no authentication. On Hydrogen it is live with zero setup because the route proxying that enables it is on by default. Alongside it sits a capability profile at .well-known/ucp that lets you enumerate what a merchant will answer before you write a single selector.The questions only this guide would ask, and which are unanswered. What is the rate limit? Is there per-store opt-out, and how many merchants have used it? Does it return the same catalogue as the rendered page, or a subset shaped for conversational use? And what happens to all of the above when merchants notice the traffic?
Treat this as a lead with a measurement attached. The mechanism is documented and verifiable. The limits are not, and this guide does not publish numbers it has not measured. The card that follows this one should be an afternoon with a live store, a request counter and a diff against the rendered catalogue.
robots.txt file does not, and that collectors lose when they kept going after being told to stop. Then it stops at the case summary. What a working data business actually does that week is undocumented, and the community's confident answers to it frequently contradict the case law this guide cites.Whether to reply at all, and what we already deleted it does to your position — it can read as good faith or as an admission depending on what you deleted and when. That is a question for a lawyer, but the engineering decision of whether deletion is even possible is yours, and it is decided months earlier by how you stored provenance.
The pattern problem. Complying quietly with one letter may be cited by the next sender as evidence you knew. That is an argument for a consistent written policy rather than case-by-case improvisation.
The architecture consequence, which is the part this guide can own. Structure the crawl so one domain can be dropped without a rebuild: per-source lineage on every row, per-source scheduling, and no transform that silently merges sources into an unattributable blend. If you cannot delete one source cleanly, you have no operational answer to a letter, only a legal one.
The scale of the change. A June 2026 measurement across roughly 32,000 domains found close to half already supporting hybrid post-quantum key exchange, and Akamai made it the default for client-to-Akamai connections at the end of January 2026. This is not an edge case any more; it is the majority path on protected infrastructure.
What to do about it. Check what your impersonation library actually sends rather than what its profile name claims. A profile called
chrome131 that predates the PQ rollout will happily identify itself as a browser that no longer exists. This is the same failure as the Camoufox correction above: a version label is not a measurement.The other number worth holding. Only 34% of sites probe
navigator.webdriver at all. The single most-patched property in the stealth ecosystem is checked by barely a third of the web, which says something uncomfortable about where collective effort has gone.What this is good for. It gives a denominator. When a vendor quotes a success rate, or a blog post claims a technique works, you now have a population baseline to compare against: a plain headed Chromium already passes roughly 93% of the Tranco 10K, so any claim of a large improvement needs to say on which subset.
Decompression bombs are the cheap one. A few kilobytes of gzip can expand to gigabytes, and a fleet of browsers each holding one is an outage you paid for. Cap the decompression ratio and the response size, not just the timeout.
The controls are boring, specific and mostly missing. An egress allowlist on the browser fleet so it can reach targets and nothing else, especially not link-local metadata. Response-size and ratio limits before parsing. A patch cadence for headless Chrome that matches the one you would apply to a public-facing service, because that is effectively what it is. And no credentials on the machines that render pages.
The unfillable case is more common than people admit. Price pages, inventory levels and rankings frequently have no history endpoint. If your collector was down for six days, those six days do not exist and no amount of re-queueing conjures them. That should be a stated property of each source, recorded when you onboard it, not discovered during an incident.
Note the one partial exception. Sometimes the history does exist — just not on the source. See the next card, because a public archive is the only answer this problem has.
1.234,56 read as one point two three four. 03/04 read as March when the site meant April. A price in the currency of the exit node's locale rather than the market you are tracking. Full-width CJK digits that a naive parser drops.Where it comes from, specifically. Locale-dependent thousand and decimal separators. Ambiguous date orders with no timezone. Currency inherited from geolocation rather than declared by the page. Character-set guessing when the server lies about its encoding, producing mojibake that looks like a data-entry error rather than a pipeline bug. And multi-market sites serving different units to different exits.
The fix is a declaration, not a checker. Record the expected locale, currency, date order and encoding per source at onboarding, and assert against that declaration rather than against generic plausibility. Then a mismatch is a failure instead of a number.
The caveats, stated honestly, because they decide whether this works for you. Crawl cadence is nowhere near daily for most domains, so gaps in the archive are common. Coverage is popularity-biased, so the long tail is thin exactly where you often need it. JavaScript-rendered content is largely absent, which rules out a lot of modern commerce pages. And the licence terms on what you may do with the data are not the same as the terms on data you collected yourself.
The judgement. It is a poor primary source and an excellent reconnaissance and recovery tool. Checking costs one API call. Not checking has cost people weeks.
Why this matters more than an average anti-bot product. It is reproducible, published and free. The LLM-honeypotting card in this guide describes sites that lie; this is that idea peer-reviewed, benchmarked and shipped as something a defender can install on a Friday.
The defensive read for a collector. If your pipeline is LLM-to-Script with cached deterministic extractors — the pattern this guide already recommends on cost grounds — you are in the category that structural obfuscation targets most directly. Regenerating an extractor against an obfuscated page produces a confident, wrong extractor. Your verification gate against golden fixtures is the control that catches it.
robots.txt works perfectly on the bots that were going to comply anyway, and that remains true for anonymous collectors. But for an identified crawler that wants network-wide access, a declared preference now has a consequence attached to ignoring it. The file did not gain teeth; the identity layer beside it did.The category split is the practical part. A site can say yes to search indexing and no to training while allowing agent traffic, which means the same crawler is welcome or unwelcome depending on what it says it is for. Declaring purpose accurately becomes an access strategy rather than a courtesy.
And a directory now exists. Classifications are searchable, so how your crawler is categorised is a public fact about your business that customers and targets can look up. That is a reputational surface most collectors have never had to manage.
Why payment rails beat web standards here. A web-infrastructure identity standard needs adoption on both sides and gives a site little immediate reason to comply. A payment-network standard arrives attached to the money: merchants adopt it because the acquirer, the scheme and the chargeback rules say so. Enforcement follows the settlement, not the specification.
What that means for a collector. Identity is bifurcating. Anonymous collection keeps getting harder and cheaper to detect. Identified, purpose-declared, payment-attested agent traffic is being actively courted. Those are not two points on one spectrum — they are separate roads, and the second one is being paved much faster.
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. A reverse-engineering map is not a wire capture. Agent tools that read HAR files or attach to Chrome over CDP help with the first fifth of this loop, finding endpoints and the scripts that call them, and are weak on the other four by design. REA’s passive CDP capture, for example, keeps method, status and URL, drops headers after a short allowlist, never retains cookies or authorization values, and reduces JSON bodies to value-free shapes, so header order, casing and token chains are simply not in its output. Its HAR reader redacts known auth fields but keeps custom ones: in one offline run on a synthetic file, the cookie and bearer token vanished while an x-signature value survived. Use such a tool to decide which request matters, then diff the real wire in a recording proxy. Check the released build too: the endpoint-template view in REA’s docs was missing from npm 6.4.0.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.The Firefox bug as evidence of the layer below. Fingerprint's team found that Firefox-based browsers let one site link activity to another across a live browser process — the exact cross-context linking that cookies were supposed to be required for, achieved with none. Responsibly disclosed to Mozilla and the Tor Project, fixed in Firefox 150 and ESR 140.10.0. It matters here because it's a reminder that the browser's attack surface is enormous and shifting: the same primitives that leak a user's identity across tabs are the ones a detector can read to tell your automated browser from a human's, and they change version to version. Whatever you audited on the last build is not guaranteed on the next.
Sources: Glassbox (via The Register); Fingerprint research, fixed in Firefox 150 / ESR 140.10.0.
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.
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.
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
requests to async httpx buys nothing there; across hundreds of independent hosts async does pay, roughly 100 seconds sequential against about one second for 200 requests at 500 ms each. Two details decide whether you get that: httpx.AsyncClient caps its pool at 100 connections by default, so 200 hosts run in two waves unless you raise max_connections, and an async client with no limiter sends everything at once rather than queuing politely. Use a per-host limiter that spaces request starts, because a plain sleep after each response adds the response time to every interval. This is arithmetic from the library defaults, not a benchmark.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. Add one check that item counts alone miss: a search that returns exactly its cap. Many search surfaces truncate per query (a map search stops near 120 places, Google’s Places API Text Search at 60 across all pages), and a truncated query looks like a healthy run with a plausible count. Partition the query space instead, by map tile, price band, category or date range, so each partition returns fewer results than the cap, and flag any partition that comes back full as truncated so the job splits it again. Then dedupe on a stable ID, because neighbouring partitions overlap. One practitioner reported about 100,000 Google Maps listings in under 48 hours by gridding three cities into bounding boxes and running the cells in parallel; it is a single unpublished job, so measure your own overlap and completeness against a known total before trusting the count. 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. Test that reflex against a deliberate fixture before a real site tests it for you. Public practice targets now ship crawler traps as named endpoints: a page documented to return 429 with Retry-After: 5 after three requests in ten seconds, a robots.txt Crawl-delay, a response that drips one byte per second for sixty seconds and a body of about 50 MB. The last two catch the timeout most clients get wrong: a per-read timeout never fires on a slow drip, so set a total deadline and a maximum body size as well. Confirm the fixture actually fires before trusting a green run: in one spot check, six back-to-back requests to that rate-limit page all came back 200, and a throttle that never saw a 429 has not been tested. The second branch of that rule is the limiter that does not refuse you at all. Reacting to block signals assumes the server will tell you, countably, that you have gone too far; some are configured to queue the over-limit request instead. Polymarket documents exactly this on its public read endpoints: per-endpoint ceilings on sliding ten-second windows — 4,000 requests per 10 s on the Gamma API in general, 500 on /events, 300 on /markets, 1,500 on the order book — enforced at the IP layer by an edge provider, and then the sentence that matters, that once you exceed a limit requests are throttled, delayed or queued, rather than immediately rejected. That word is load-bearing: it promises delay ahead of a possible refusal, not the absence of one. Under that regime nothing arrives to count, and the only quantity that moves as you cross the line is service time — which makes per-endpoint latency the alarm, on the dial this section just warned you against. The two rules do not conflict: latency can never tell you that you are blocked, but where the block is implemented as a queue, it is the only thing that tells you that you are close. A 402 is neither of those signals, so keep it out of the back-off loop. It is not a rate limit, and waiting will not change it. On Cloudflare’s pay per crawl the response carries a crawler-price header (for example USD 0.01) and often a crawler-error code, and only a crawler registered with Cloudflare and signing with Web Bot Auth can answer it, by resending with crawler-exact-price. The x402 variant instead puts a base64 payment object in a PAYMENT-REQUIRED header. For an ordinary scraper the right handler identifies which dialect it is, logs the price and path, marks the URL as closed rather than failed, and moves on. Retrying a 402 burns proxy bandwidth on an answer that cannot change, and counting it as a block inflates the ban rate your throttle is steering by. Scope it before you trust it. The same operator publishes per-signer buckets that do return 429 with Retry-After, explicitly separate from the IP layer, and a sibling domain with a flat twenty requests per second and no queueing. And note that this is read from published limits, not exercised against a live API.
localStorage or IndexedDB, and Playwright’s storageState() includes IndexedDB only when you pass indexedDB: true (added in v1.51). Where the site rotates refresh tokens, a snapshot taken on Monday and restored on Thursday restores a token the server already retired. This is one vendor founder’s production experience with no published failure rates; the Playwright option is documented, the rest is a reasoned pattern. A later hands-on test supports the restore half and adds a warning about the first login. A saved profile (cookies and storage restored into a fresh hosted browser) opened a grocery shop as the logged-in user from a different city’s residential IP, confirmed by the app’s own cart call rather than the cookie jar. On a second site, logging in from a data-centre Linux browser triggered phone 2FA, and the account then kept demanding 2FA even from the owner’s normal browser. Do the first login of a real account from a coherent identity, expect a flagged account to stay flagged for a while, and keep the human handoff ready. The site never rotated its token during the test, so the Monday-snapshot failure was neither confirmed nor ruled out.Know what your framework now opens by default. Since Scrapy 2.19 the live-debugging step can go further than the shell: a built-in remote-control extension, on by default, lets a tool you authorise execute Python inside the running crawl, so you can inspect a stalled scheduler queue or try a selector on a response already in memory instead of stopping and rerunning. Treat that as a capability you opt into, not one you inherit. Its only gates are a random localhost port and a bearer token written to a file in your user profile, so anything running as your user on that host can use it, including an agent that is reading untrusted scraped pages. Defaults cap each call at 30 seconds and 64 KB of output, which limits noise, not what the code may touch. On shared runners, CI machines and containers that mount your home directory, set
REMOTE_CONTROL_ENABLED = False unless you mean to attach, and keep the job-files directory out of anything you sync or share. The exposure is inferred from documented defaults rather than a demonstrated exploit, but a crawl process usually holds credentials and proxy secrets, which is reason enough to close it by default. That last one carries a sharp diagnostic: if the shell does not contain data you can see in a browser, that is evidence about the page's delivery path, not an instruction to add a browser. The data is arriving by some route you have not captured yet (an XHR, a JSON island, a different endpoint), and finding it is usually cheaper than rendering. Keeping the first project boring, settings for behaviour, a pipeline for shared rules, real logs, and configured feed exports, is what turns a script that scraped a site once into a project you can trust to run again.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.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.
.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 Nagpalplaywright.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 · YouTubeOct 2026 update. Containers give each worker the same isolation at process level: its own memory cap, logs and restart policy. Docker’s
on-failure policy restarts only on a non-zero exit, and only after the container has run 10 seconds. One trap for browser workers: Playwright’s Docker docs warn Chromium can run out of memory and crash without --ipc=host, and recommend --init and a pinned image tag.camoufox or rayobrowse to generate sessions, then curl_cffi with the extracted cookies for bulk collection. Rotate sessions every 30-50 requests.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.
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. The same logic applies to search APIs feeding an agent: compare them on cost per correct answer, not cost per call, and count the LLM tokens spent reading results. On one September 2026 benchmark with a fixed model and 3 runs per configuration, the cheapest provider on every board (TinyFish, $0.035 to $0.219 median per task) completed 79% of hard-retrieval tasks with fetch against the leader’s 83% at more than twice the cost, a good trade, but scored only about 27–30% F1 on 45 multi-hop questions against 46–48% for the leaders. If a wrong or missing answer sends work to a human or forces a retry, the cheaper API can end up costing more. Treat this as one operator’s snapshot on narrow task sets, and rerun a sample of your own queries before you switch. Also price the option of not calling a live search at all. Most search APIs for agents are, underneath, a scrape of Google, so their price tracks Google’s defences. A self-hosted web index turns that per-call cost into fixed hardware and a freshness problem. One vendor documents an 8 TB target index on about 10 TB of NVMe, 64 GB RAM and a small GPU, English only, with closed platforms such as Facebook, Reddit, LinkedIn and Instagram generally excluded; an employee demonstrated a 2.3 TB slice running offline. Choose it when queries are about stable reference material or when no traffic may leave your network. Keep a live path when answers must be current to the day, because a local index knows nothing after its last shard. Retrieval quality for such slices against a live engine has not been published, so test on your own queries." Here is the honest math, with the caveat that exact prices move constantly, so treat these as orders of magnitude, not quotes. When an agent reaches a scraping API through an MCP server, find out first who pays when a fetch fails. The model picks the URLs and can retry a blocked one many times over. In one vendor’s comparison of nine scraping MCP servers, only the publisher stated a policy, that its own failed requests are not billed, and the other eight entries say nothing on it. Read each vendor’s pricing terms for failed, blocked and CAPTCHA responses before you let an agent spend, cap requests per task, and use a token you can rotate. The test that comparison itself recommends is cheap: put the same 20 to 50 real URLs through every shortlisted server, and count pages that came back with real content, not responses that came back at all.
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
ETag and Last-Modified, send them back as If-None-Match and If-Modified-Since, and an unchanged page can answer 304 with an empty body. In one test of 368 top homepages and 224 inner pages, about 40% returned a 304 where the full page had a median of roughly 45 KB, which matters most on per-GB residential bandwidth. About half the pages sent neither header, so the saving is site-specific, and some validators are broken: 17 homepages rotated their ETag on every request and a few returned 304 after their text had changed. Send both headers, check an ETag twice before trusting it, and expect a lower 304 rate at real recrawl intervals than in a seconds-apart test. A pipeline with a low success rate pays for every failure twice: once in the wasted request, once in the retry. The reverse also holds when you buy rather than build: a high success rate on a hardened target is itself what you are paying for, and its price moves when the target moves. Treat any Google feed from a third party as a cost line that can jump without notice. Write the per-1,000 price into your unit economics, check the vendor’s page on a schedule, and keep a second provider or your own fallback tested on a sample of real queries so a switch is a configuration change. In October 2026 one SERP vendor said keeping Google at 95%+ success had raised its infrastructure bill 3–4×, and its listed price became 7 credits a search, about $1.05 per 1,000 on pay-as-you-go. That is one company’s account with no published costs, but the direction matches the extra browser, warm-up and /goto traffic a working Google request now needs. A new line item may join these for declared crawlers: paying the site per request. Cloudflare’s pay per crawl, still a closed beta, bills a registered crawler each time a page returns 200 with a crawler-charged header, including every re-crawl of a page that has not changed; error responses are not billed, and /robots.txt, /sitemap.xml and a few other paths stay free. Since June 2026 a site’s origin can set its own price per response, so one domain can charge differently by path. Cloudflare sets no spending cap for you. If you ever run a paying crawler, the unit that matters becomes cost per changed record: conditional requests and a change-detection pass before a paid fetch save more than any price negotiation, and a hard per-domain budget belongs in code, not in a dashboard.
/_next/data/<buildId>/<page>.json returns the page props without the HTML around it. On TED’s homepage it came to roughly 289 KB against 589 KB for the full HTML, about half the bytes and nothing to parse. It is not universal, since only 9 of 26 Pages Router homepages in one test answered it, and the build ID changes with every deploy, so read it fresh from __NEXT_DATA__ and retry once on a 404. Embedded payloads also carry more than the page shows, one pricing page held 23 price entries while displaying 4, so keep only the public fields you need. Reserve the browser for pages that genuinely need JavaScript. Measured across the top of the web, that set is small. In one September 2026 test of 544 Tranco top-1,000 homepages, only 5% needed JavaScript to show their text, 62% had it in the plain HTML, and another 3% carried it as JSON in the page source; 54 of 62 Next.js homepages were server-rendered. The browser path cost a median 9.5 seconds per page against 0.7 for a plain request, about 14 times longer, though that includes a load wait a tuned scraper can shorten. So size the browser tier from a measured sample of your own targets rather than from the assumption that modern sites need one, and expect product and search pages to need it more often than homepages do. Blocking heavy resource types on the requests you do render is one of the cheapest wins available. Block by destination as well as by type, then measure what is left, because asset blocking alone rarely gets a script-heavy page close to a plain request. In one vendor-run October 2026 benchmark on a hotel search behind Kasada, a default headless browser moved 4.0 MB of residential proxy traffic per search; blocking images, fonts, media and third-party scripts cut that to 1.1 MB, but 76% of the remainder was the site’s own JavaScript. A request-based client fetching only the challenge and the results HTML used 178 KB, about 22 times less than the default browser. If the data is already in the HTML or an API response, that gap is the case for building the request path. Count the bytes at a proxy you control: the same test found Chrome DevTools reported 25% less than the proxy carried, since it skips TLS handshakes, request headers and aborted downloads.optimizationguide-pa.googleapis.com before you navigate anywhere; one proxy vendor’s open benchmark counted 43.2 MB of a 43.4 MB idle window on a browser parked on about:blank. On a metered residential exit that is billed traffic no target ever sees, and a pipeline that mints a new profile per identity pays it every time. Refuse that host at your proxy layer or local relay, and count bytes per identity so the warm-up, the search and the browser’s own housekeeping show up as separate lines. The figure comes from one setup and one Chrome build, so measure your own idle profile before you rely on it.A perspective worth holding while you read the rest of this guide
A warning about the one clean number: cost per thousand requests
($/success ÷ $/GB) × 1,000,000 KB, which at published rates falls near 160 KB against a $9.40/GB unblocker and near 106 KB against a committed dollar per thousand successes. Above that size trimming payload cuts the bill; below it, only cutting request count does. Then there is the tier. One vendor publishes its price list as a difficulty scale: $0.13 per thousand at the simplest rung rising to $1.27 at the hardest, and $1.01 to $16.08 once a browser renders — one vendor, one unit, a 124× spread, wider than the gap between two vendors. Those labels are that vendor’s own commercial classification of your target rather than an independent measure of it, and the cheapest rung sits roughly eight times below the one-to-five dollars per thousand in the table above, which describes hard targets. Credit multipliers work the same way: one provider bills a URL at a credit and another at twenty, and the unaudited but plausible reading is that the multiplier reports which route that provider bothered to engineer for your domain, so a 20× on an obscure target may be a fallback nobody was paid to optimise rather than proof the page is hard.
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
close() is documented to do no more than disconnect from the browser server: the process, the profile and the meter all survive it. Measured against one cloud provider, reconnecting after a close took about 1.55s with the page still open and a value written twenty seconds earlier still readable in localStorage, against about 6.05s once the provider’s own stop call had been issued, which is the cold-provision cost of a session that genuinely died. Two non-overlapping bands, so a warm reconnect is a usable leak probe — measure your own, since the ratio scales with your round-trip time — and put the close and the stop in a finally, or every crashed run keeps billing. The second habit is chattiness. Reading twenty cards field by field issues on the order of eighty protocol calls and took about 16s a run; the same extraction as a single page.evaluate() took a quarter of a second. Locally a chatty chain is untidy. Over a wire it is the invoice. Both figures are single runs from one client-host pair, published by the provider on its own blog.
The observation that makes it more than vendor talk: brand equity does not transfer across the split. Years of proxy performance awards and a top response-time record mean nothing to a developer discovering tools through GitHub stars and framework docs — and conversely the AI-native players' positioning means nothing the first time their retriever hits a DataDome wall. It's the same fault line as the API-vs-scrape scorecard in §01: the hard, valuable 20% is exactly the part the shiny layer skipped, and it's where the work still is.
Why it matters if you're choosing tools, not selling them. An “AI-native” data product that demos beautifully on a blog and a docs site is telling you nothing about whether it can reach the retailer, the marketplace or the forum you actually need. Test any retrieval or agent product against your hardened targets before you believe its coverage — because most of them, by this account, inherited the reachable web and quietly redrew the boundary of “the web” around it.
Source: Gabrielé Vitké (proxy + AI vendor).
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.
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.
What the blocking actually did. Cloudflare began blocking AI crawlers by default across its share of the web unless a customer allows them, and the July plan said that from 15 September 2026 mixed-purpose crawlers that combine search and training — Googlebot included — would be blocked on ad-supported pages unless separated first. Correction, 30 Sep 2026: what shipped on the day was softer — new domains only, a robots.txt “Disallow AI Training” default on ad-supported sites, and Googlebot blocked only where an owner explicitly selects Block. 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 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.
Oct 2026 update. One small site’s first three viral days, self-reported: a Gemini-backed Q&A over Pakistani government pages (itself a .gov.pk crawler) served 480K+ requests, with 10,900+ from “unidentified bots” (about 2.3%, far below the half above), 1,300+ attack requests in three flood attempts and two prompt injections reaching the model; none worked. The defences were cheap: per-user daily limits, no stored data, links only to government sites. n=1, bot definition unstated, launch traffic skews human.
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. Add a fourth question before any of those: what personal data did you just take on, and did you need it. Under strict readings of privacy law, dropping a field after parsing does not undo having processed it; one practitioner’s reading of Saudi Arabia’s PDPL argues that a scraper which inspects sensitive data in order to discard it has already processed it. The engineering answer is to minimise at the selector rather than in a later cleaning step: extract only the fields your purpose needs, avoid pulling whole bio or profile blocks where health or religion can hide, write down what you collect and why, and set a retention limit before the first row lands. That record is also what a legitimate-interest assessment asks for.
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. When a site sends no validators you still have to download, but you can skip the expensive work after it: hash the extracted main text and reprocess only when that hash changes. Hash the text, not the raw HTML. Across 368 top homepages fetched twice a second apart, the visible text changed on 28 while the raw HTML changed on 150, through scripts, tokens and attributes nobody reads, so an HTML hash would re-parse or re-embed about five times as many pages for no real change. That is one run on homepages and the gap will vary by site, but it shows which side of the extractor the comparison belongs on. Time-series collection inverts that last rule. When you track engagement on social posts, prices or rankings over time, the re-scrape is the data: append one row per observation with its own observation timestamp, keyed on the source’s stable ID, and never upsert it away. Keep publication time and observation time in separate columns, because a post published yesterday gains engagement today and the two answer different questions. Dedup your own fetches on the platform’s post ID, but keep independent reposts of the same text, since spread across accounts is the signal a trend pipeline exists to see; a content hash would collapse it. Log collection coverage per window beside the counts, so a scraper outage does not read as a quiet hour and the backfill as a spike. Resolve relative stamps like “3h” at scrape time and remember they are truncated to their unit. This comes from a tutorial, not a measured pipeline.Data poisoning: when the bypass succeeds but the data is fake
CloseSpider inside a Scrapy item pipeline does nothing, because process_item is wrapped in a bare except Exception that swallows it — close the spider through the engine instead. The published thresholds, it should be said, are the author’s own guesses tuned against a demo site he built, at a vendor that integrates the model.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. An empty result is only trustworthy once you have proved there was something to find. Before accepting zero rows, make a second, independent call that confirms the thing you asked about exists and is non-empty: the group, category or search you requested, with a count or size you can read. Facebook group feeds show why. One practitioner traced an identical 200 with an empty post list to three separate causes in a single session: a group slug that never resolved to its numeric ID, a login wall served in place of a public group, and a rotated GraphQL doc_id. The details lookup for the bad slug said not found while the feed endpoint happily paginated nothing. The rule the write-up lands on is the one to copy: an empty feed from a group that exists and has members is a failure to raise, not a result to store. It is one vendor-authored account with no failure rates, so treat the three causes as a checklist, not a census. Gate every write on a content classification, never on the status code. For Cloudflare, run a challenge check first, before looking at the status at all, because the same interstitial arrives as either 200 or 403. Match the body case-insensitively for challenges.cloudflare.com/turnstile, cf-turnstile, __cf_chl_, cf_chl_opt, a Just a moment title or Checking your browser, and treat a cf-mitigated: challenge header as decisive; server: cloudflare and cf-ray only say who fronts the site. Classify into three outcomes, challenge, ok and hard_block, and give each its own action: reroute only challenges to a rendering path, back off on hard blocks, and run the rerouted response through the same detector before storing it. Keep the detector a pure function so saved challenge pages become regression fixtures, and chart the reroute rate, since a jump is usually the first sign a target changed its protection. The marker list tracks Cloudflare’s current markup, so re-check it against a fresh challenge page before trusting it. 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. Then measure freshness and coverage against something you did not produce. Report record age as a distribution: a median of three days can hide a 99th percentile of fourteen months, so publish the tail, not the crawl schedule. For coverage, compare against an independent source: one LexisNexis team used Common Crawl as a reference and found hundreds of missing records caused by its own extraction code overwriting rows, a bug no status code or fill-rate check had flagged. Compute totals and reconciliations in ordinary code rather than asking a model, because a model can return 4,523 for 4,532 without any error. Track cost per accepted record rather than cost per page: in the same account, a relevance-only crawl fetched 91% fewer pages yet cost about three times as much. These are speakers’ own figures from one conference, relayed secondhand, so treat them as patterns to test rather than benchmarks.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. Add page size and status variants to the same alert. A blocked response does not have to look like a challenge: one request to Amazon’s homepage in October 2026 returned a 200 with a 3,781-byte “continue shopping” gate and no CAPTCHA wording at all, where the real page runs to hundreds of kilobytes, and in a 12-store run the day before, the same block arrived as a 202 on nine stores and an empty 200 on three. Track response size per page type and flag a sudden drop, count a field the page always carries, and treat any 2xx as unverified until that check passes. A missing item does not tell you why it is missing, because a JavaScript-rendered listing fails the same count, so keep failed samples to separate blocking from rendering. These are single-run observations from one location, not rates. 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.
That counter has an edge: a great deal of what gets crawled has no records in it. Documentation and reference pages have no fields to tally and no row count to compare against yesterday, so the alarm collapses back into the check it was meant to replace: the file exists, therefore the run worked. Half that file can be navigation chrome, the one code sample you needed missing, and nothing downstream will say so. The substitute for a schema is a named artefact chosen before the crawl: pick one reference paragraph and one code block off the live page, and let the run pass only if both survive extraction. Wire the failure path first: store each failed URL with its error message, or an empty result arrives looking like a success. Judge one thing at a time. Hand every candidate the same URL list when you are testing extraction and the same seed and scope when you are testing discovery, or a poor result will not say which half broke; and settle acceptance before you time anything, so you only measure output you agreed to keep. Keep the pilot in proportion. The published demonstration is one command against a static documentation site with no JavaScript and no bot protection — depth one, three pages, concurrency one — run by an author who builds one of the tools he compares, with no timings and no head-to-head. It establishes a discipline, not a success rate: a local sample tells you whether the output fits your application, never what it costs per month at volume.
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. Add page size and status variants to the same alert. A blocked response does not have to look like a challenge: one request to Amazon’s homepage in October 2026 returned a 200 with a 3,781-byte “continue shopping” gate and no CAPTCHA wording at all, where the real page runs to hundreds of kilobytes, and in a 12-store run the day before, the same block arrived as a 202 on nine stores and an empty 200 on three. Track response size per page type and flag a sudden drop, count a field the page always carries, and treat any 2xx as unverified until that check passes. A missing item does not tell you why it is missing, because a JavaScript-rendered listing fails the same count, so keep failed samples to separate blocking from rendering. These are single-run observations from one location, not rates. 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.
That counter has an edge: a great deal of what gets crawled has no records in it. Documentation and reference pages have no fields to tally and no row count to compare against yesterday, so the alarm collapses back into the check it was meant to replace: the file exists, therefore the run worked. Half that file can be navigation chrome, the one code sample you needed missing, and nothing downstream will say so. The substitute for a schema is a named artefact chosen before the crawl: pick one reference paragraph and one code block off the live page, and let the run pass only if both survive extraction. Wire the failure path first: store each failed URL with its error message, or an empty result arrives looking like a success. Judge one thing at a time. Hand every candidate the same URL list when you are testing extraction and the same seed and scope when you are testing discovery, or a poor result will not say which half broke; and settle acceptance before you time anything, so you only measure output you agreed to keep. Keep the pilot in proportion. The published demonstration is one command against a static documentation site with no JavaScript and no bot protection — depth one, three pages, concurrency one — run by an author who builds one of the tools he compares, with no timings and no head-to-head. It establishes a discipline, not a success rate: a local sample tells you whether the output fits your application, never what it costs per month at volume.
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. A block can arrive the same way. Some targets answer a refused request with a 200 and a page saying you have been blocked, and a pipeline that trusts the status code will parse it, store it and count it as a success. Check the body for block markers and for the fields you expected before you count a fetch as a success, and keep those failures out of your success rate. Do not trust a 200 to mean the page exists either: once per site, request a random path that cannot exist, store what comes back, and compare every scraped page against that “not found” fingerprint (same title, similar word count) and against the homepage. A redirect from a deep URL to the homepage or a login page is a miss, not a success. Asked for a made-up path, 43 of 371 top sites (12%) still answered 200 in one October 2026 run, and a rule ladder built that way left 1 of the 43 labelled a success. On 13 sites, Instagram and Twitch among them, the missing page and the homepage were the same script shell, so no HTML comparison can decide; label those unclear rather than guessing, then render or check for the field you came for. Those rules were scored on the sites they were written for, so expect more misses on new targets. Practice sites now include exactly this case, a 200 whose page reads “You’ve been blocked”, next to a cookie that keeps marking a client as blocked on later visits, so the check can be tested before a production target forces it. The same care applies to your own loop guard. A listing that sends you back to page 1 after every detail visit makes records reappear, and a guard that fires on any repeated record will call that an infinite loop and stop the crawl early. Record the page each item was first seen on and never overwrite it; a known item on an earlier page means you were bounced, so paginate forward and resume from the same row. Reserve the loop verdict for a repeat on the same page with no forward progress. One engineer who hit this on a government property register reports the change cut the crawler’s state from seven variables to three, and that a synthetic two-page listing bouncing after every record went from a loop failure to 20 of 20 records. That is one site and one test; it assumes stable record keys and ordering, because a listing that reorders between visits will look like a bounce.
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.Three ways to actually measure recall. Facet decomposition: slice the same catalogue by a different dimension — brand, price band, date — and check the union exceeds what a single traversal returned. Identifier probing: where identifiers are dense, sample the space directly and see how many resolve to items your crawl never found. Cross-source reconciliation: compare against a second source that should agree, including the site's own sitemap and its declared result counts.
Then write the number down. Estimated recall belongs next to freshness in what you report, because a customer told a dataset is complete when it is 70% complete has been misled regardless of how clean every returned row is.
The fake content takes recognisable forms: fabricated product listings and identifiers, prices attached to nothing real, contradictory dates and specifications, and invented relationships between entities.
Detection, and this is the actionable half. Seven signals: a crawl count far exceeding what
sitemap.xml declares; URLs that nest deeper without ever reaching a terminal page; fluent text containing no verifiable facts; near-identical content under many distinct URLs; and internal contradictions across pages that should agree. Then run near-duplicate detection — SimHash, MinHash, n-gram comparison or embeddings — because exact-match deduplication misses generated pages that differ by a few words. “A response can look valid, parse correctly, and still lead the scraper nowhere.”What a citation actually asserts. Provenance, not currency. It says a document at that URL contained this claim at the moment it was indexed. It says nothing at all about whether the document still says it. Those are different guarantees, and RAG systems routinely present the first as if it were the second.
The fix is the fix you already know. Store fetch time alongside the text, treat staleness as a first-class field, and alert when the gap between index time and now exceeds what the source's own change rate justifies. An unchanged index over a changing source is the same alert as an unchanged file from a live feed — which is to say, a loud one.
Oct 2026 update. A Crawlbase tutorial (vendor; code MIT) attacks this gap by re-fetching each cited page at check time instead of trusting the index: it keeps the 3 passages closest to the claim, asks a judge model for a label and quote, and accepts the label only if the quote is literally on the page. Its only numbers come from a fixture dry run, and the substring check proves the quote exists, not that it supports the claim.
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.
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 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.
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.
Oct 2026 update. A Shopee data vendor says most public Shopee scrapers began returning empty fields after a stricter anti-bot change in September, a promotion with no evidence. It fits the record: an Apify actor dated an emergency update to 29 April 2026, and another says product pages moved behind a JavaScript-signed token in mid-2026. Nothing independent confirms September, so the lesson stays field-level checks.
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. That durability is worth pricing precisely, because every alternative to it keeps a phone in the loop and inherits the phone’s clock. An oracle costs you one function call per signature and stays fast. Driving the app’s interface to produce the same header costs you the interface, and the per-step latencies published for natural-language Android agents make that concrete: roughly 3–5 s a step on a fast model, 15–40 s on a strong one, so a hundred-step run is 25 to 67 minutes on a single device. That is arithmetic on a vendor’s own published rates, not a figure measured under load in any published run. Set it against an offline signer that mints a header in milliseconds and the comparison stops being interesting. The second cost is visibility rather than speed. Reading the screen means a declared accessibility service, and a declared service is enumerable by the app the same way root is, so the tooling you installed to drive a hardened target becomes one more thing that target can see. Drive the interface when the signer is a bytecode VM you genuinely cannot rewrite. Do not drive it because Ghidra looks like work.
.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.
jadx-headless-mcp 0.7.1 on JDK 17 or newer, and the bundle-unminifying and firmware helpers run on Linux only. Its APK inspection reports signature verification as not performed, and its Frida tools run scripts you write inside the target with the app’s own network authority, so use an emulator and a test account, never a personal device. The project’s disclaimer refuses help bypassing bot protection or access controls on systems you are not authorised to test. Treat it as a faster way to read an app, and run any decision to replicate a signed call past the legal section first.
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
.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 courseScraping 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 two dozen principles, and those principles attach to the layers a request is judged at. 203 nodes, 365 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. For fundamentals on video, add these channels, and expect most of them to teach on targets with little or no bot protection, so pair them with the detection sections of this guide. Michael Mintz: the SeleniumBase author on UC Mode and browser automation. Cole Medin: AI agents, including Crawl4AI with n8n. freeCodeCamp: a 4.5-hour Scrapy course covering proxies, deployment and scheduling, made with ScrapeOps. NeuralNine: Python and AI tutorials, with some scraping. Corey Schafer: Python fundamentals, Requests and BeautifulSoup. Keith Galli: project-based scraping and data analysis. Unclecode: a small channel from the Crawl4AI author on crawling and Docker deployment. Apify, Firecrawl and Playwright: official channels, so expect their own products. John Watson Rooney, further down this section, is still the place to start.
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.Oct 2026 update. Austin ran 7–8 October at Brazos Hall; the European edition is in Dublin on 10–11 November. Day 1 is five workshops, including one on browser automation for dynamic sites, aimed at pages that render in your browser and come back empty to the scraper, the Tier 3 case in the decision flow. Day 2 is talks, which the organiser says go to the Extract Summit YouTube channel.
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 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.
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.
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.
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.