Undetectable Web Scraping Browser Automation: 2026 Guide
Most scraping projects do not die at the parser. They die about three weeks in, when a site that cheerfully served 40,000 pages starts handing every request from your box a challenge, and nobody on the team can say what changed. The selectors still work. The code still runs. The data stopped arriving.
That gap — between code that works and code that keeps working — is what undetectable web scraping browser automation is actually about. It is less a library choice than an architecture decision: whether your crawler presents as a fleet of plausible devices with histories, or as one machine wearing forty paper masks.
This is the practitioner version. What detectors really look at, which automation tells cannot be fixed from userland JavaScript, how to structure identities and proxies so they do not correlate, how to run a few hundred concurrent profiles on hardware you can afford, and how to notice you are blocked before your dataset quietly fills with empty rows.
What 'undetectable' actually means
Nothing is undetectable in an absolute sense. A platform with your full traffic history in front of it and an engineer motivated to look will eventually cluster you. The realistic goal is narrower and far more achievable: be indistinguishable from ordinary traffic at the price the defender is willing to pay.
That reframing matters, because it tells you where to spend. Anti-bot vendors operate on unit economics. Every check they run costs latency, false positives, and support tickets from real customers who got a CAPTCHA on checkout. They spend cheaply and broadly first, expensively and narrowly later. Your job is to stay inside the cheap-and-broad band, where the only available verdict is 'this looks like a person on a laptop in Ohio'.
Four layers, not one
A modern detection stack reads you on four independent layers. Miss any one and the others do not save you.
- Network and transport. IP reputation, ASN class, whether that address has ever behaved like a home connection, plus the TLS ClientHello fingerprint (the JA3 and JA4 family) and the HTTP/2 fingerprint — SETTINGS frame values and order, window sizes, pseudo-header order.
- Browser surface. The fingerprint proper: canvas and WebGL rendering output, audio stack, installed fonts, screen geometry, timezone, languages, hardware hints, Client Hints. If this is new to you, start with browser fingerprinting explained for beginners and then come back.
- Automation artefacts. Anything that reveals a debugger, a driver, or injected code —
navigator.webdriver, DevTools protocol side effects, patched getters, stealth-plugin residue. - Behaviour. Timing distributions, mouse and scroll traces, the shape of your navigation graph, requests per identity per hour, whether you ever load an image you did not need.
Almost every team I have watched fail over-invested in layer 2 and ignored layers 1 and 3 completely. A gorgeous, internally consistent fingerprint delivered over a flagged datacentre IP by a stack that announces a debugger on page load is not stealth. It is a very well-dressed alarm.
Blocking is a decision, not a switch
The other thing to internalise: you will often not be blocked, you will be degraded. Real responses that are subtly wrong.
- The page returns 200 with 60% of its usual byte count and no price block.
- Search results come back, ranked from a stale cache, missing the personalised tier.
- The API responds, but the
nextCursorfield is always null, so your crawl silently ends at page one. - Everything is perfect for the first eleven requests of each session and thin thereafter.
This is why 'we are getting 200s' is not evidence of anything. Content invariants are evidence. More on that when we get to telemetry.
Why headless browsers get caught
The obvious flags, briefly
Standard automation announces itself. navigator.webdriver is a specification-mandated property that reads true when the browser is under remote control via the automation switch — one line to read, impossible to argue with. Alongside it: the HeadlessChrome token in the user agent, the automation infobar, missing or stub window.chrome, permission and plugin combinations that no shipping browser produces.
Chrome's newer headless mode closed much of the historic gap, and if that were the whole story stealth would be a solved problem. It is not, because two harder classes remain.
The DevTools protocol tell most guides skip
Here is the one that ends most projects and almost never appears in tutorials.
When an automation framework calls Runtime.enable on the Chrome DevTools Protocol, it asks the browser to start behaving as if an inspector is attached: execution contexts get announced, and console arguments get serialised for preview. That serialisation is observable from inside the page. The classic probe is to construct an object with a getter on one of its properties, hand it to console.debug, and see whether the getter fires. With no DevTools window open, a getter that runs means something is listening. Variations read Error stack behaviour or look for the isolated world that evaluation helpers create.
The consequence is structural rather than cosmetic. Any framework whose ordinary page.evaluate path depends on the Runtime domain is emitting that signal from the first navigation of every session. You cannot patch it away in page context, because it is the browser process doing the talking, and the thing doing the asking is your own client.
So the design rule for undetectable browser automation is a subtractive one: never enable Runtime on the driving path. Drive with DOM, Input, Page, Network and Target only. Resolve nodes, dispatch trusted input, watch navigation, read the wire. If you genuinely need to evaluate an expression — sometimes you do, for a stubborn <select> or a computed value — make it an explicit, opt-in call that is logged as such, so you know exactly which of your jobs paid that price and on which domains. Never on Google properties, never on a login flow, never as a default.
Patched JavaScript loses to property archaeology
The second hard class: spoofing from inside the page leaves marks on the page.
Suppose you override navigator.hardwareConcurrency with a getter. You now have to survive all of the following:
Function.prototype.toStringon your getter, which a native function would render as[native code]— maskable, but the mask itself is a function with a stack.- Property descriptor comparison: enumerability, configurability, and whether the property sits on the instance rather than on
Navigator.prototypewhere the real one lives. - Descriptor order.
Object.getOwnPropertyNamesreturns a stable order in a real browser; re-defining properties reshuffles it. - A fresh same-origin iframe, whose
contentWindow.navigatoryour patch never touched. Comparing window to iframe is two lines of code and catches an enormous amount of tooling. - Worker scope.
new Workergets an untouched global. A document claiming 8 cores while its worker reports 24 is not a subtle discrepancy. - Timing. Native property access is a handful of nanoseconds. A JavaScript getter behind a Proxy behind a toString mask is measurably slower, and detectors do run that loop.
Every one of these disappears when the values come from the browser's own C++ layer instead of a script. There is nothing on the JS heap to inspect, toString is genuinely native, the descriptors are the real ones, iframes and workers inherit correctly because they inherit from the same source. This is the single biggest architectural difference between an antidetect engine and a stealth plugin, and it is why Dual Login applies fingerprints natively at engine level with zero injected JavaScript on the fingerprint path. If you want the mechanics of how a fingerprint is composed and swapped, how to change browser fingerprint walks the individual surfaces.
The network layer betrays you before any JavaScript runs
The fastest rejection happens before your DOM parser ever wakes up.
Request libraries have their own TLS handshakes. Python requests, httpx, Node fetch, Go's default transport, curl — each produces a ClientHello whose cipher list, extension order, supported groups and ALPN differ from Chrome's, and each therefore has a stable JA3/JA4 hash that anti-bot services have catalogued for years. You can spend a month perfecting a canvas hash and be filtered on the handshake.
This is the strongest argument for scraping through a real browser rather than a HTTP client: you get the correct TLS and HTTP/2 fingerprints for free, by construction, because it is the same code path a real user's browser takes. It is also the reason header coherence matters. Sec-CH-UA must agree with your user agent, which must agree with the platform your fingerprint claims, which must agree with the fonts you ship and the timezone you report. Incoherence is cheaper to detect than any single value is to fake.
The architecture that survives: identity-first scraping
Stop thinking in requests. Think in identities.
A profile is a device, not a session
An identity is a durable bundle: one fingerprint, one persistent data directory, one proxy exit, one accumulating history of cookies and localStorage and cached assets and visited URLs. It has an age. It has habits. It survives your process restarting, your deploy, and your laptop rebooting.
That durability is the point. A first-time visitor with an empty cookie jar who deep-links straight into a category page at 03:41 UTC, pulls 200 product pages in nine minutes and never returns is a recognisable pattern no matter how good the fingerprint is. The same fingerprint on its fourteenth visit, arriving with a session cookie it earned last Tuesday, is boring. Boring is the goal.
The practical shape: one OS process per profile, its own user data directory, its own memory. That gives you real isolation — a poisoned cookie jar or a corrupt cache damages exactly one identity — and it means a crash takes one worker rather than your fleet. It is also why the model in what is an antidetect browser and how does it work maps so directly onto scraping, even though most people meet these tools through multi-account work.
Native fingerprinting, generated coherently
A fingerprint is not a list of values, it is a set of values that have to agree with each other. Generate them together or do not bother.
- GPU vendor and renderer strings must match a real driver combination for the platform you claim, and the ANGLE backend must be one that platform actually uses.
- Screen resolution, available area and device pixel ratio must be a shipping configuration, and the window you open must fit inside it.
- Font list must match the OS. A Windows machine without Segoe UI, or a Mac with Calibri and no Helvetica Neue, is a fabrication.
- Timezone,
Intllocale,Accept-Languageand the proxy exit country must tell one story. - Hardware hints (
hardwareConcurrency,deviceMemory) must suit the device class. A claimed MacBook Air with 32 logical cores and 512MB of memory is not a device.
And critically: stability over churn. Rotating fingerprints per request feels like stealth and is the opposite. Real devices change slowly — a browser update here, a monitor plugged in there. Sixty identities that each hold one fingerprint for months look like sixty people. Six thousand fingerprints appearing once each from a small IP range look like exactly what they are.
Persist everything a real browser would persist
If your container is ephemeral, your identities are not identities. Persist the data directory: cookies, localStorage, IndexedDB, service worker registrations, the HTTP cache. Cached assets matter more than people expect — a returning visitor issues conditional requests and gets 304s, while a fresh profile downloads every sprite and font from scratch, and that difference is visible in your request pattern for the first ten seconds of every session.
Snapshot sessions on a schedule as well as on close, and treat the snapshot as the source of truth for that identity. If you run workers across several machines, you need last-writer-wins ordering on session data or two boxes will happily overwrite each other's fresh login with a stale copy.
One proxy per identity, for the life of the identity
The pairing rule is simple and violated constantly: an identity keeps its exit IP, and an exit IP serves one identity at a time. Cross-multiplying forty fingerprints against forty rotating IPs generates 1,600 device-location combinations, which is a statistical signature no residential neighbourhood produces.
Proxy tier is a budget decision, not a quality ranking:
| Tier | Typical cost | Best for | Fails at |
|---|---|---|---|
| Datacentre | Cheapest, flat-rate | Public docs, sitemaps, open APIs, dev and staging | Anything with a reputation database; whole ranges are pre-flagged |
| ISP / static residential | Mid, flat-rate per IP | Long-lived logged-in identities, steady daily harvests | Sites that expect mobile-heavy audiences; limited geo choice |
| Rotating residential | Per-GB, expensive | Wide geographic coverage, hardened retail and travel targets | Session stability — rotation mid-flow breaks token continuity |
| Mobile (4G/5G) | Most expensive per GB | The hardest targets, mobile-first platforms | Cost; latency; shared CGNAT means noisy neighbours |
Sticky sessions are non-negotiable for anything stateful. If your provider rotates the exit inside a session, your authenticated crawl will trip an IP-change check mid-flow, and you will spend a week blaming your cookie handling. The trade-offs in depth, including how to hold a residential session stable across hours, are in antidetect browser with residential proxies.
One more: mask WebRTC to the proxy exit. WebRTC will otherwise hand out your real address through an entirely different code path than your HTTP stack, and no proxy setting stops it. And to answer the question that always arrives here — a VPN does not solve any of this, because it changes one IP for every profile on the machine while leaving all four detection layers otherwise identical. The difference is worth understanding properly.
Driving the browser without leaving prints
Keep the protocol surface narrow
Attach briefly, do the work, detach. A CDP client held open for the lifetime of a session is a state you have to defend; a short connection that dispatches input and closes is much less to explain. Enable only the domains you need. Never hold a debugger open through a login.
Trusted input, real event loops
Dispatch input through Input.dispatchMouseEvent and Input.dispatchKeyEvent rather than synthesising events in page context. Browser-dispatched input arrives with isTrusted true and goes through the real hit-testing and focus machinery; element.click() from JavaScript does not, and the difference is a one-line check.
Then make the motion plausible. Not elaborate Bezier theatre — just not teleportation. Move in several steps toward the target with a little overshoot, pause a beat before the click, type with variable inter-key delays in the 60–180ms band with the occasional 400ms think-pause, and scroll in wheel increments rather than jumping scrollTop. Add a realistic dwell time on pages you would actually read. It costs you seconds per page and buys the single cheapest behavioural cover available.
Selector-free interaction for when the DOM fights back
Some targets randomise class names per build, or render critical controls into canvas, or shuffle the DOM specifically to break automation. Two escape hatches help.
First, resolve and act atomically. A node id fetched from DOM.getDocument becomes invalid the moment the document is re-fetched, so if two of your workers interleave against one tab you will chase 'could not find node with given id' forever. Serialise per tab: one mutex, resolve-and-act inside it.
Second, for anything the DOM will not give you, screen-space interaction with OCR works better than it sounds. Screenshot the viewport, find the text, click the coordinates with a trusted mouse event. It is slower and it needs a retry policy, but it is genuinely selector-independent and it survives redesigns that break every CSS path you own.
Take the data from the wire, not the DOM
The highest-leverage change most scrapers can make: stop parsing HTML.
Modern pages fetch their own JSON from their own internal API, authenticated with their own cookies, in a format that is far more stable than the markup around it. Turn on network capture, let the page do its thing, and read the response bodies. You get typed fields instead of scraped strings, you get pagination cursors the UI hides, and you stop rewriting selectors every time the design team ships.
From there a hybrid pipeline becomes possible, and this is where throughput really arrives:
- Use the browser for the hard part — the login, the token mint, the cookie set, the anti-bot challenge that only a real engine can pass.
- Extract the resulting credential and endpoint shape.
- Bulk-fetch the boring pages, re-injecting the tokens as they rotate.
The caveat is the one from earlier: the moment step 3 leaves the browser, you inherit your HTTP client's TLS fingerprint. On soft targets that is fine and the throughput gain is enormous. On hardened ones, keep the fetches inside the browser context, where the handshake is already correct. Recording a flow, letting a dependency analyser work out which response value feeds which later request, then replaying it is exactly this pattern made repeatable.
Scaling to hundreds of profiles
The RAM and disk math
Plan capacity from measured floors, not hope. A freshly launched profile sitting on a blank page costs roughly 250–400MB of resident memory. A heavy single-page app with maps and video players will pass 700MB on its own. Site isolation multiplies renderer processes per origin, so a page with a dozen third-party embeds spawns a dozen processes.
Turning off site isolation and capping the V8 heap and media caches — a low-RAM mode — changes the arithmetic dramatically, at the cost of some in-browser security separation you do not need when every profile is already its own process with its own data directory. Under those settings, planning at about five concurrent profiles per 4GB is honest for light pages; on a 32GB box expect 25–40 depending on target weight. Keep the GPU enabled: WebGL spoofing needs a real rendering path, and software rendering will both slow you down and produce renderer strings that do not match the hardware you claim.
Disk is the silent killer. Each profile accumulates cache, IndexedDB and service worker storage indefinitely. Cap per-profile cache, prune on a schedule, and monitor free space as a first-class metric — a full disk manifests as corrupt cookie jars and mass logouts, which look exactly like a ban.
Concurrency without correlation
Running 300 profiles is easy. Running 300 profiles that do not look like one program is the work.
- Jitter every start. Forty sessions beginning in the same 200ms window is a fingerprint of its own. Spread them over minutes with randomised gaps.
- Randomise the work queue per profile. If every identity walks your URL list in the same order, the queue is your signature.
- Vary the day. Give profiles schedules. Some work mornings, some evenings, some take a day off. If your target correlates by time-of-day, a fleet that all wakes at 02:00 UTC is trivially clustered.
- Do not share viewports. Window geometry should follow each profile's claimed screen, with the small variation a real user's window has.
- Never share a data directory. Two workers on one cookie jar corrupt both identities and eventually merge them in the target's eyes.
Request budgets and warm-up
Treat each identity as having a daily allowance, and set it from what a human could plausibly do. A shopper views 40–80 product pages in a session, not 4,000. If you need 100,000 pages a day, that is a fleet-size problem, not a rate-limit problem, and pretending otherwise just burns identities.
New identities need warming. A profile whose very first act is a deep authenticated crawl is spending trust it has not earned. Give it a few days of light, shallow, normal-looking activity — homepage, a search, a couple of pages, leave. Then scale it up. The pattern is exactly the one account operators use to keep platforms calm, and the reasoning transfers directly: the practices in how to avoid account bans are the same trust-building mechanics applied to a different goal.
Canaries and block-rate telemetry
You cannot manage what you do not measure, and status codes are not measurement.
Log, per request: HTTP status, response byte size, time to first byte, whether a challenge marker appeared in the DOM, and — this is the important one — whether your content invariants held. Assert that the price field exists, that the result count is above zero, that the pagination cursor is populated. A run where 100% of requests returned 200 and 40% failed the price assertion is a soft block, and the assertion is the only thing that will tell you.
Tag every one of those records with the identity, the proxy, the proxy ASN and a hash of the fingerprint. Now you can slice. Is the failure concentrated in one subnet? One fingerprint template? Profiles created last Tuesday? That is the difference between a five-minute fix and a fortnight of guessing.
Finally, hold back about 5% of your fleet as canaries that scrape nothing. Each one fetches a single known-stable control page on a slow cadence and records status, size and latency. When your yield drops, the canaries tell you instantly whether the site changed or you did — a distinction worth its weight in engineer-hours at 2am.
Comparing the approaches
Indicative rather than precise; the exact numbers depend entirely on your target's defensive posture. The ordering, however, is consistent across every project I have seen.
| Approach | Automation artefacts | TLS / HTTP2 match | Fingerprint depth | Pages per host per hour | Holds up on hardened targets |
|---|---|---|---|---|---|
| HTTP client + rotating datacentre IPs | None to find | Wrong, catalogued | None | Very high | No — filtered on handshake |
| Headless Chrome, defaults | webdriver, Runtime, UA token | Correct | Default = host machine | High | No |
| Headless + stealth plugin | Patched getters, iframe and worker gaps, Runtime | Correct | Shallow, JS-level | High | Sometimes, briefly |
| Real browser + native fingerprint, ephemeral profiles | Minimal if Runtime avoided | Correct | Deep | Medium | Partially — no history to lean on |
| Antidetect engine, persistent profiles, paired proxies | Minimal by design | Correct | Deep, coherent | Medium | Yes, and it stays that way |
| Hybrid: browser mints tokens, HTTP bulk-fetches | Minimal | Correct only in the browser leg | Deep where it counts | Very high | Depends on the endpoint's own checks |
The pattern to read out of that table: throughput per host and durability trade against each other, and the hybrid row is the only place you get both — at the price of a more complex pipeline and per-endpoint testing.
A pipeline that holds up
Stage 1 — recon, by hand, once
Before you write a line of crawler, open the target in one profile and watch the network panel like a human being. What does it fetch? Is there a JSON endpoint behind the render? What sets the session cookie, and how long does it live? Where does the challenge appear — first paint, first interaction, or the eleventh request? Copy a request as cURL and see what it needs. An afternoon here routinely saves a fortnight of blind retries.
Stage 2 — build the identity fleet
Mint profiles with coherent fingerprints spread across the device mix your target actually serves. If 60% of the site's audience is on Windows Chrome and 25% on mobile Safari, match that distribution — a fleet of exclusively identical Windows desktops is unusual in a way that shows up in aggregate. Pair each profile with its own sticky exit in a country consistent with its locale. Persist the directories somewhere durable. Warm them.
Stage 3 — harvest
Work the queue with per-identity budgets and jittered scheduling. Read from network capture where you can and from the DOM where you must. Snapshot sessions periodically so a crash does not cost you a warm identity. Write raw payloads to storage before parsing them — when the schema shifts you will want to reprocess yesterday, not re-crawl it.
Stage 4 — the repair loop
Assume decay. Sites change, defences tighten, proxy pools go stale. Build the loop in from the start: invariant assertions fail, telemetry names the slice, you retire the affected identities or swap the affected subnet, and the queue backfills the gap. A scraper without a repair loop is not a system, it is a snapshot with a cron job attached.
Anti-patterns
Every one of these is something I have watched cost someone real money.
- Fingerprint churn. Rotating identity per request. Real devices are stable; churn is itself the anomaly.
- Cross-multiplying identities and IPs. N fingerprints times M exits equals N×M impossible devices.
- Timezone and locale drift. Exit node in São Paulo,
Intlreporting America/New_York,Accept-Language: en-US, fonts from a German Windows install. Any one of those is survivable. Together they are a confession. - A shared 'unique' canvas seed. Ten thousand profiles with the same non-default canvas hash is a far stronger cluster than ten thousand profiles with the honest default.
- Worker mismatch. Patched values in the document, real values in
new Worker. Two lines to detect. - Retry storms. Getting challenged and immediately hammering the same identity turns a maybe into a certainty, and often escalates from identity-level to subnet-level enforcement.
- Mixing states in one identity. Scraping logged-in and logged-out from the same profile, or research and production from the same pool.
- Ephemeral profiles. Fresh cookie jar every run means every visit is a first-time deep-link visitor, forever.
- Ignoring soft blocks. Shipping a dataset that is 30% thin pages, and finding out from the client.
- One machine, no isolation. Forty tabs in one browser with one data directory is not forty identities. It is one identity behaving very strangely.
Where the line is
Worth saying plainly, because the technique is neutral and the use is not.
Scraping publicly accessible information is broadly lawful in many jurisdictions, and cases like hiQ v LinkedIn have pushed back on the idea that a terms-of-service breach is automatically a computer-misuse offence — but 'broadly' is carrying a lot of weight there, the law differs by country, and the analysis changes completely once authentication, personal data or copyrighted content is involved. This is not legal advice, and if the project is commercially material you want a lawyer who knows your jurisdiction rather than a blog post.
What is unambiguous, and what serious operators do anyway:
- Read robots.txt and honour it where you have no compelling reason not to.
- Rate-limit as a courtesy, not just as camouflage. Do not degrade a service for its actual users.
- Do not collect personal data you have no lawful basis to hold. GDPR and its equivalents apply to scraped data exactly as they apply to data you were given.
- Do not circumvent paywalls or authentication you have no right to pass.
- Honour takedown and deletion requests promptly, and keep provenance so you can act on them.
- Cache aggressively and re-crawl sparingly. It is cheaper for you and lighter on them.
If you want to see how much of you is visible before you start, the EFF's Cover Your Tracks is still the most honest free mirror on the web.
Choosing tooling
Evaluate with your own target, not a feature list. The questions that actually separate the field:
- Is fingerprinting applied natively, or injected as JavaScript? Test it: compare
navigatorvalues between the window, a fresh same-origin iframe, and a worker. Any disagreement is injection. - Can you drive a profile over CDP without enabling Runtime, and does the tool tell you when a call needs it?
- Do profiles persist fully — cookies, localStorage, IndexedDB, cache — and can you move an identity to another machine without losing its session?
- What is the honest concurrency ceiling on your hardware, measured, not marketed?
- Is there an API surface identical to the UI, so nothing is click-only?
On cost: per-profile pricing punishes exactly the thing scraping needs, which is many cheap identities. Read the terms carefully. Cheaper Multilogin alternatives that actually work covers where the pricing models diverge, and if you are still choosing between the obvious incumbents, GoLogin vs AdsPower is the comparison worth reading before you commit a year.
FAQ
Is web scraping with an antidetect browser legal?
The browser is a tool; legality depends on what you collect, from where, and under what terms. Gathering public information is broadly permitted in many jurisdictions, but authentication, personal data, copyrighted material and contractual terms all change the analysis, and it varies by country. Nothing here is legal advice — for anything commercially significant, get counsel who knows your jurisdiction, and in the meantime respect robots.txt, rate limits and privacy law.
Can I not just use Playwright or Puppeteer with a stealth plugin?
For soft targets, yes, and you should — it is cheaper and simpler. It stops working on hardened ones for two structural reasons: the frameworks' default evaluation path enables the DevTools Runtime domain, which is detectable from inside the page, and plugin-based spoofing lives in JavaScript, where it can be caught by descriptor comparison, cross-frame checks and worker scope. Neither is a bug you can patch; they are consequences of where the code runs.
How many profiles can one machine actually run?
Budget around five concurrent profiles per 4GB of RAM for light pages with low-memory settings on, and expect 25–40 on a 32GB box. Heavy single-page apps can halve that. Watch disk as closely as memory — accumulated per-profile cache fills a drive quietly and the symptoms look exactly like a ban.
Do I need residential proxies to scrape?
Not always. Public documentation, sitemaps and open APIs are usually fine over datacentre IPs, which are an order of magnitude cheaper. Move up to ISP or residential exits when your telemetry shows IP-correlated failure — not before, because per-GB pricing on a large crawl adds up fast. Whichever tier you pick, keep the session sticky and keep one identity per exit.
How do I tell whether I am being blocked or soft-blocked?
Assert on content, not on status. Check that the fields you need actually exist, that result counts are non-zero, that pagination cursors are populated, and that response sizes sit in their normal range. Then keep a handful of canary profiles fetching one stable control page on a slow schedule, so that when yield drops you know immediately whether the site changed or you did.
Does running a real browser make scraping too slow?
Per page, yes — a real engine with human-plausible pacing is slower than a raw HTTP fetch. Per usable page over a month it is usually faster, because you are not repeatedly rebuilding a pipeline that got banned. Where you need genuine volume, use the hybrid pattern: let the browser handle login, challenges and token minting, then bulk-fetch the easy pages, testing carefully because that leg carries your HTTP client's TLS fingerprint rather than the browser's.
The short version
Undetectable web scraping browser automation is not one clever trick, it is the absence of contradictions. A coherent fingerprint applied natively, so there is nothing in JavaScript to inspect. A narrow DevTools surface that never enables Runtime on the driving path. Persistent profiles with real histories, each paired to one stable exit IP for its whole life. Request budgets a person could plausibly produce. And telemetry that asserts on content, so you learn about degradation from your dashboard rather than from your client.
Get those five right and the exotic problems mostly stop happening. Get them wrong and no amount of canvas noise will save the run.
If you are putting a fleet together, Dual Login is built for exactly this shape of work: native engine-level fingerprinting with no injected JavaScript, a fully persistent data directory per profile, per-profile proxy pairing with WebRTC masked to the exit, and an automation API that drives tabs over a deliberately narrow CDP surface. Spin up a handful of profiles against your own target, watch what your invariants say, and let the data decide.