Distributed Web Scraping Architecture Best Practices (2026)
Most scraping projects don't fail because somebody wrote a bad CSS selector. They fail six weeks in, when the target site swaps its anti-bot vendor, the proxy bill triples overnight, and the single oversized server running four hundred headless tabs falls over at 3 a.m. with nobody watching. The code was fine. The architecture was the problem.
This guide collects distributed web scraping architecture best practices that actually hold up in production: how to split work across machines, how to manage the identities your workers browse with, how to handle the failures that will absolutely happen, and how to see a ban wave coming before it lands. It's written from the operator's side of the fence — the person who has to keep the pipeline green on a Tuesday, not the one drawing boxes on a whiteboard and walking away.
Why single-machine scrapers hit a wall
A scraper on one machine is a script. A scraper spread across many machines is a system, and systems have failure modes that scripts never meet. It helps to be precise about which walls you're actually going to hit, because each one demands a different architectural answer.
The first wall is throughput. Browser-based fetching is expensive — a rendered page costs real CPU, hundreds of megabytes of RAM per browser process, and seconds of wall-clock time. One machine tops out somewhere between a few dozen and a couple hundred concurrent browser sessions depending on RAM, and no amount of async cleverness changes the physics. Past that point you scale out or you don't scale.
The second wall is identity concentration, and it's the one people underestimate. Every request from one machine shares an IP path, a TLS stack, and — if you're running browsers — a set of fingerprints that all look suspiciously alike. Modern detection systems don't need to catch a single request doing something wrong; they cluster traffic by shared traits and score the cluster. A single machine hands them a beautifully tight cluster on a plate.
The third wall is blast radius. When your only machine gets its IP range flagged, its disk full, or its kernel OOM-killed, you have zero throughput and no partial degradation. Distributed systems fail too, but they can fail 10% at a time.
Distribution solves all three problems — but only if you design for it deliberately. Bolting more servers onto a script-shaped architecture usually multiplies the problems instead of dividing them: now you have five machines double-fetching the same URLs with clashing sessions, and five machines' worth of ban risk correlated through one shared proxy account.
The anatomy of a distributed scraping system
Every scraping operation that scales past a few million pages a month converges on the same five components, whatever names the team ends up giving them. If one of these is missing, you'll find its logic smeared across the others, which is where the bugs live.
The URL frontier
The frontier is the brain. It decides what gets fetched next and — just as importantly — what doesn't get fetched yet. A competent frontier deduplicates URLs so two workers never fetch the same page in the same window, enforces per-domain politeness budgets, prioritises high-value or time-sensitive pages over backfill, and re-schedules pages on a revisit cadence appropriate to how often they change.
Keep the frontier separate from the workers. The moment scheduling decisions live inside worker code, two workers will eventually disagree about them, and you'll spend a weekend discovering that your 'deduplication' was per-process. A Postgres table with the right indexes handles frontiers into the hundreds of millions of URLs; Redis sorted sets work well for hot, priority-ordered subsets.
The queue
Between the frontier and the workers sits a message queue. Teams burn astonishing amounts of energy debating Kafka versus RabbitMQ versus Redis Streams versus SQS, and the honest answer is that any of them works if you get three properties right. You need acknowledgement semantics with a visibility timeout, so a job grabbed by a worker that dies quietly re-enters the queue instead of vanishing. You need per-domain partitioning or per-domain rate keys, so one slow, heavily-defended site can't starve every other target of workers. And you need a dead-letter path for jobs that keep failing, because a poison message retried forever is a self-inflicted denial of service.
Fetch workers
Workers should be deliberately dumb. Take a job, look up the identity assigned to it, fetch the page through that identity, push the raw result downstream, acknowledge the job. That's it. Statelessness is the entire trick: a stateless worker can be killed, redeployed, autoscaled or moved between machines without ceremony, because everything that matters — the schedule, the session, the results — lives somewhere else.
The one piece of state a worker does handle is the browser session itself, and the right move is to make that someone else's problem too. More on that in the identity section, because it deserves its own.
Parsing and storage
Parse away from the fetch path. Fetching is scarce, expensive, ban-prone capacity; parsing is cheap, infinitely retryable CPU. If your worker fetches a page and then spends 800ms extracting fields from it, you've cut your effective fetch throughput nearly in half for no reason. Push raw HTML (or a compressed snapshot of it) into object storage, drop a pointer onto a parse queue, and let a separate fleet chew through extraction.
Storing raw snapshots also buys you something priceless: when a site changes its markup and your extractor breaks — which happens constantly — you fix the parser and replay from storage. No re-fetching, no re-burning identities, no gap in the dataset.
Monitoring and control
The fifth component is the one skipped most often: a control plane. At minimum you want per-domain dashboards for success and block rates, a global kill switch, and per-domain pause buttons. When something goes wrong at 2 a.m. — and it will — the difference between a ten-minute incident and a lost weekend is whether you can stop the bleeding for one domain without taking the whole system down.
Pick the right fetch tier for every target
Not every page deserves a browser. A rendered browser fetch costs somewhere between 50 and 200 times more than a plain HTTP request once you account for CPU, RAM, proxy bandwidth and time, so routing every URL through your most expensive tier is how scraping budgets die. The best architectures treat fetch method as a routing decision, made per domain and revised from live feedback.
| Tier | Relative cost per 1k pages | JS rendering | Detection resistance | Use it for |
|---|---|---|---|---|
| Plain HTTP client | 1x | None | Low — TLS and header fingerprints give libraries away | Sitemaps, APIs, RSS, static HTML, permissive sites |
| Vanilla headless browser | ~50x | Full | Low to medium — headless leaks and automation flags are well catalogued | JS-heavy sites with weak or no bot defences |
| Antidetect browser profile | ~80–150x | Full | High — a unique, internally consistent fingerprint per profile, no automation tells | Logged-in sessions, account-bound data, hard anti-bot targets |
The routing rule that works in practice: start every new domain on the cheapest tier and escalate on evidence. When plain HTTP starts returning challenge pages, JavaScript-rendered shells or suspiciously short responses, promote the domain to a browser tier. When a vanilla headless browser starts eating CAPTCHAs, promote it to full antidetect profiles. Record the tier per domain in the frontier so the whole fleet routes consistently, and periodically probe downward — defences get relaxed too, and there's money in noticing.
Identity is an architectural layer, not an afterthought
Here's the thing most distributed scraping write-ups skip entirely: at scale, the biggest architectural mistake isn't the queue choice or the storage engine. It's treating identity as a per-request accessory — a User-Agent string pulled from a list, a proxy pulled from a pool — instead of a first-class layer with its own lifecycle.
Modern detection doesn't read your User-Agent and call it a day. It fingerprints canvas rendering, WebGL renderer strings, audio processing quirks, installed fonts, screen metrics, timezone, languages and a few dozen other signals, then cross-checks them all for internal consistency. A request claiming to be Chrome on a MacBook while rendering canvas like a Linux server in a datacenter is more suspicious than an honest bot. If this layer is new to you, our plain-English primer on browser fingerprinting covers the mechanics, and pointing a stock browser at the EFF's Cover Your Tracks tool is a bracing five-minute demonstration of how identifiable a default setup really is.
The architectural consequence: identities must be created, assigned, persisted and retired by the system — not improvised inside workers.
One coherent identity per session, not per request
Rotating everything on every request is a classic self-own. Real users don't change devices, IP addresses and cookie jars between page loads; traffic that does gets clustered and flagged precisely because it's incoherent. The pattern that survives is session-scoped identity: a fingerprint, a proxy exit, and a cookie jar that stay glued together for the life of a browsing session, then rest before reuse.
This is exactly the job an antidetect browser exists to do. Dual Login runs each profile as a real, separate browser process with its own data directory, a unique fingerprint applied natively inside the engine rather than through injected JavaScript that detection scripts can unmask, and its own proxy binding. Your orchestrator asks for profile 47 and gets the same coherent device every time — same canvas noise, same WebGL strings, same fonts, same timezone matched to the same exit IP. If you've been hand-rolling spoofing with browser automation plugins and losing, our guide on how to change your browser fingerprint explains why the injection approach keeps getting caught and what native application does differently.
Size your identity pool deliberately. A useful starting ratio is three to five profiles per concurrent worker slot per hard target, so every identity gets rest between sessions. Identities are a consumable with a burn rate; budget them like proxies, not like config.
Persist sessions like they're money
On any site worth scraping behind a login, the login endpoint is the single most heavily defended page. A session that survives — cookies, localStorage, the works — across worker restarts andmachine migrations means you almost never touch it. A pipeline that logs in fresh every run is a pipeline that trains the target's models on your login pattern until it stops working.
Distribute this properly and the profile becomes portable: worker A opens profile 47, browses, closes and the session state syncs; worker B on a different machine can open profile 47 tomorrow with the login intact. The rule that makes it safe is single-writer discipline — exactly one machine may hold a given profile open at a time. Two workers browsing the same session concurrently produces conflicting cookie writes, and the losing write is the one you needed. Dual Login enforces this with a cross-machine lock and last-writer-wins timestamps on session state; if you build your own, build that lock before you build anything else.
Assign identities from the frontier, not from a random function
random.choice(profiles) on each worker means the same identity can be picked by three workers in the same second while another sits idle for a week. Uneven wear is how you get a mystifying pattern of some accounts dying and others never being exercised.
Make identity assignment a scheduling decision. The frontier hands out a job and the identity to use, applying whatever rules matter: cooldown since last use, historical success rate per identity per domain, geographic match between the identity's claimed locale and its exit IP, and a hard concurrency cap of one. Now the identity layer is observable and tunable, which means you can improve it. A random function inside a worker can only be prayed at.
Proxy architecture that doesn't bankrupt you
Proxies are usually the largest line item in a scraping budget and the most common source of self-inflicted bans. Both problems come from treating the proxy pool as an undifferentiated blob.
Match proxy type to target, per domain
Datacenter IPs are cheap and fast and are pre-flagged on a great many consumer sites — the ASN alone gives them away. Residential IPs cost far more per gigabyte but pass as ordinary home connections. Mobile IPs are the most expensive and the most trusted, because carrier NAT means thousands of real users share one address and blanket-banning it isn't viable for the site.
The cost-efficient architecture routes by domain, not globally: datacenter for permissive targets, residential for defended ones, mobile reserved for the handful of targets that reject everything else. Record the choice per domain in the frontier alongside the fetch tier. For the specifics of pairing sticky residential exits with persistent browser profiles — session duration, rotation timing, and the geo-consistency traps — see our antidetect browser with residential proxies playbook.
Sticky sessions for logged-in work, rotating for anonymous crawling
Rotating IPs mid-session on a logged-in account is one of the loudest signals you can emit. Real accounts don't teleport between cities between page loads. Use sticky sessions — an exit IP held for the duration of your browsing session — anywhere a cookie jar is involved. Save fast rotation for anonymous, stateless crawling where each request genuinely is independent.
Bandwidth discipline is a code problem
Residential proxy billing is per gigabyte, and a browser without instructions will happily download every image, font, video and analytics beacon on the page. Blocking media and third-party trackers at the request level routinely cuts bandwidth by 60 to 80% on content-heavy sites, which on a large residential bill is real money.
Be careful here. Blocking too aggressively is itself a fingerprint — a browser that never requests a single image or ad script looks nothing like a human visitor. Block heavy media and known analytics domains; let CSS, fonts and first-party scripts through. Measure the block rate before and after, not just the bill.
Never trust a proxy's geography without checking
Providers mislabel exits constantly, and a profile claiming Europe/Berlin behind an IP that geolocates to Ohio is trivially detectable — the timezone-versus-IP mismatch is one of the cheapest checks a detection script can run. Verify the exit IP's real location at session start and align the profile's timezone, locale and language headers to it. Dual Login does this at launch — the geo lookup drives the timezone and language settings, and WebRTC is masked to the proxy exit so the real IP can't leak past it, which is the difference between an antidetect browser and a VPN in one sentence.
Concurrency, politeness and rate control
The fastest way to get a domain-wide ban isn't a weird fingerprint. It's volume.
Rate-limit per domain, globally
Worker-local rate limits are a trap. Twenty workers each 'politely' limited to one request per second are collectively hammering a site twenty times a second. Rate limits must live in shared state — a Redis token bucket keyed by domain is the standard implementation — so the limit means what it says regardless of how many workers exist.
Set the budget from observed behaviour, not optimism. Watch the 95th-percentile response time: when it starts climbing, you're near the site's comfortable capacity and should back off before the site does it for you. Rising latency is the polite warning that precedes the impolite block.
Adaptive backoff beats fixed schedules
A fixed request rate is a fingerprint in itself; nothing on the human internet arrives at perfectly regular intervals. Add jitter, and more importantly, make the rate respond to signals. Increasing 429s or 403s, challenge pages appearing, response times climbing, or content getting subtly thinner — each should reduce concurrency for that domain automatically. Recover slowly: multiplicative decrease, additive increase, the same shape as TCP congestion control, because it's the same problem.
Also, actually read robots.txt and respect Crawl-delay where it's set. Beyond the ethics, it's free intelligence about what the operator considers acceptable, and staying inside that line keeps you out of the traffic patterns their alerting is tuned to catch. MDN's overview of robots.txt is a good refresher on what the directives actually bind.
Spread work across time, not just machines
A hundred thousand pages fetched in a two-hour burst looks like an attack. The same hundred thousand spread across twenty-four hours with a diurnal curve that roughly tracks the site's real audience looks like traffic. If your business needs the data by 9 a.m., start at midnight — don't start at 8 and sprint.
Failure handling: assume everything breaks
At scale, rare failures are constant failures. A one-in-ten-thousand event happens a hundred times a day at a million pages a day. Design accordingly.
Classify failures before you retry them
Blind retries turn a transient hiccup into a self-inflicted flood. Classify first, then act:
- Transient network errors (DNS blips, connection resets, timeouts) — retry with exponential backoff on the same identity. These are noise.
- Rate limiting (429, and 503 with a
Retry-After) — back off the whole domain, not just the job. One worker's 429 is information about the domain's global state. - Blocks and challenges (403, CAPTCHA interstitials, challenge pages) — quarantine the identity, retry the job on a different one, and increment a domain-level block counter that can trigger a pause.
- Genuine 404s and gone pages — don't retry. Mark them and update the frontier.
- Parse failures — never retry the fetch. The page is in storage; fix the parser and replay.
That third category deserves emphasis. When an identity gets blocked, the block belongs to the identity, not the URL. Retrying the same URL through the same profile is how a single flagged identity produces a thousand failed jobs and a metrics dashboard that looks like the site went down.
Circuit-break per domain
When a domain's block rate crosses a threshold — say 20% of requests over a five-minute window — stop fetching it automatically. Push its jobs back to the frontier, hold them, and alert. Continuing to push into a wall converts a recoverable situation into a comprehensively burned proxy pool and identity set. Ten minutes of paused throughput is cheap; re-establishing three hundred identities is not.
Make everything idempotent
At-least-once delivery means every job will occasionally run twice. Design so that's harmless: key storage writes by content hash or URL plus timestamp, make frontier updates last-writer-wins with explicit stamps, and never let a duplicate job double-charge a counter or double-append a row. This is far easier to build in from the start than to retrofit after you find duplicate rows in a customer-facing dataset.
Log the response, not just the error
When a fetch fails on a defended site, the failure page itself is the diagnostic. Store the status code, response headers, a truncated body, and — for browser tiers — a screenshot. The difference between 'blocked' and 'the site changed its login flow' and 'our proxy provider is returning its own error page' is invisible in a log line reading fetch failed, and obvious in a screenshot. Cap the retention (a few days is plenty) and sample rather than storing everything, but store enough to answer the question at 3 a.m.
Observability: the metrics that actually predict trouble
Most scraping dashboards track pages per hour, which tells you what already happened. The metrics worth alerting on are leading indicators.
Block rate per domain per hour is the single most valuable number in the system. Everything else is downstream of it. Alert on the rate of change, not just the level — a jump from 2% to 8% is a bigger signal than a domain that has sat at 15% for a month.
Success rate per identity tells you which profiles are burning and lets you retire them before they poison a whole domain's numbers. Content-length distribution per domain catches the sneakiest failure of all: soft blocks, where you get a clean 200 with plausible-looking HTML that's missing the data. A sudden shift in the median response size for a page template almost always means you're being served a decoy. Extraction completeness — the percentage of expected fields actually populated — is the last line of defence, and the one that catches markup changes before your customers do.
One alerting rule earned through pain: alert on ratios, never raw counts. Raw counts fire constantly during legitimate volume changes and get muted, and a muted alert is worse than no alert because you believe you have coverage.
Legal and ethical guardrails
This isn't boilerplate — the legal posture around scraping shapes real architecture decisions, and getting it wrong is more expensive than any technical mistake in this article.
Publicly accessible data and data behind an authentication wall are different things, legally and practically. The US Ninth Circuit's hiQ v. LinkedIn line of cases established that scraping public pages generally doesn't violate the Computer Fraud and Abuse Act, but that reasoning does not extend to circumventing authentication or ignoring an explicit revocation of access. Personal data brings GDPR and its equivalents into scope regardless of whether a page was public, which means lawful basis, retention limits and subject-access obligations become architectural requirements — you need to be able to find and delete a person's records.
Practical guardrails that belong in the system, not in a policy document: honour robots.txt and Crawl-delay; identify your crawler honestly in the User-Agent where you're crawling openly; keep request rates well below anything that could degrade the target's service; don't collect personal data you have no use for; and keep an auditable record of what you collected, when, and from where. That last one is unglamorous and it is what saves you if anyone ever asks.
The legitimate use cases for account-bound browser profiles — managing your own seller accounts, running your own ad campaigns, operating client accounts you've been engaged to operate — are exactly why per-profile isolation matters. Our Amazon seller account guide goes deep on the multi-account side of that, where the goal is keeping legitimately separate businesses from being incorrectly linked.
A reference architecture you can actually build
Putting the pieces together, here's a shape that works from roughly a hundred thousand to fifty million pages a month without fundamental redesign.
Control plane — one small, boring machine. Postgres holding the URL frontier, per-domain configuration (fetch tier, proxy type, rate budget), identity registry and job history. Redis for rate-limit token buckets and hot priority queues. A scheduler process that fills queues from the frontier, applying politeness budgets and identity assignment. A dashboard reading straight from Postgres. This machine does no fetching and should be the most stable thing you own.
Fetch fleet — N stateless workers, grouped by tier. Cheap HTTP workers can be tiny and numerous; browser workers need real RAM and get sized by how many concurrent profiles you want per host (budget conservatively — roughly 4GB per five concurrent browser profiles is a realistic floor, and going tighter means swap thrash and mysterious timeouts). Each worker pulls a job with its assigned identity and proxy, fetches, uploads the raw result, acknowledges, repeats.
Identity plane — the profile store. Fingerprints, cookie jars, localStorage, proxy bindings, cooldown state and per-domain success history, with a lock so one profile opens on one machine at a time. This is the layer that most homegrown systems lack and that most reliably determines whether the whole thing survives contact with a defended target. Dual Login is built to be this layer: profiles with native fingerprints, per-profile data directories, portable sessions that follow a profile between machines, and an HTTP API so your scheduler can launch, drive and stop profiles programmatically instead of shelling out to browser binaries and hoping.
Processing fleet — parse workers reading raw HTML from object storage, writing structured rows to the warehouse, and reporting extraction completeness back to the control plane so a broken parser shows up as a metric rather than a support ticket.
Storage — object storage for raw snapshots with a lifecycle policy (thirty to ninety days is usually the sweet spot between replay ability and cost), and a columnar warehouse for the structured output.
Sequencing the build
Don't build all of this at once. The order that avoids wasted work:
Start with the frontier and a single worker, and get correctness right — dedup, politeness, retries, idempotent writes — while the system is still small enough to reason about. Add the queue and scale to a handful of workers; this is where you discover which of your 'shared' state was actually per-process. Split parsing off the fetch path next, which usually produces a surprising throughput jump on its own. Then build the identity plane properly, before you need it, because retrofitting session persistence into a running system is genuinely unpleasant. Add adaptive rate control and circuit breakers once you have enough traffic for the signals to be meaningful. Layer in observability continuously rather than as a phase — a metric added after the incident it would have caught is a metric added too late.
Tooling: what to buy and what to build
A reasonable division of labour, learned the expensive way: build your frontier, scheduler, parsers and monitoring — these encode your specific domain knowledge and no vendor understands your targets. Buy proxies, browser identity management, CAPTCHA solving if you genuinely need it, and object storage.
Browser identity management is the one people most often try to build and most often regret. It looks like a weekend of work — patch some navigator properties, spoof a canvas, rotate a User-Agent — and it turns into a permanent maintenance obligation as Chromium ships new APIs, detection vendors publish new checks, and every one of your JavaScript-level patches becomes detectable through some property descriptor you didn't think to mask. A native implementation inside the browser engine is a fundamentally different proposition, and it isn't something a scraping team should be maintaining alongside its actual product.
If you're evaluating options, three of our comparisons cover the ground pragmatically: GoLogin vs AdsPower on the two most common incumbents, cheaper Multilogin alternatives on where the price-to-capability line sits in 2026, and what to test during a free trial — which for a scraping team should absolutely include the automation API, not just the UI, because your scheduler is the real user.
Ten mistakes that show up in almost every audit
A condensed list of the recurring ones, in rough order of how much damage they do:
- Worker-local rate limits. Twenty workers, twenty separate 'polite' limits, one very impolite aggregate.
- Rotating everything every request. Incoherent identity is more detectable than a stable one.
- Retrying blocks on the same identity. Converts one flagged profile into a thousand failed jobs.
- Parsing inside the fetch worker. Halves your scarcest resource to save a queue.
- No raw snapshot storage. Every markup change becomes a full re-fetch and a data gap.
- Trusting proxy geo labels. Timezone-versus-IP mismatch is the cheapest detection check there is.
- No per-domain circuit breaker. A bad hour becomes a burned pool.
- Alerting on counts instead of ratios. Noisy alerts get muted; muted alerts are worse than none.
- Logging in on every run. The login endpoint is the most defended page on the site. Persist sessions.
- No single-writer lock on sessions. Two machines, one profile, conflicting cookie writes, lost login.
Most of these are cheap to fix in week one and painful to fix in month six. That asymmetry is the whole argument for reading a piece like this before the build rather than during the incident.
FAQ
How many concurrent browser profiles can one machine realistically run?
Plan on roughly five concurrent profiles per 4GB of RAM as a working floor, so a 32GB machine comfortably handles 30–40 with low-memory settings enabled. CPU becomes the binding constraint before RAM on JavaScript-heavy targets. Push past those numbers and you'll see timeouts and swap thrash that look like network problems and waste days of debugging.
Should every scraping job use a browser?
No, and routing everything through browsers is the most common budget mistake in scraping. A rendered fetch costs 50–150x a plain HTTP request. Start each domain on the cheapest tier that returns complete data and escalate only when you see challenge pages, JS-only shells or suspiciously thin responses. Record the tier per domain so the whole fleet stays consistent.
What's the difference between rotating proxies and sticky sessions, and when do I use each?
Rotating proxies give you a new exit IP every request or every few minutes; sticky sessions hold one exit for the duration of a session. Use sticky anywhere cookies or a login are involved — real users don't change city between page loads. Use rotating for anonymous, stateless crawling where each request genuinely is independent.
How do I know I'm being soft-blocked rather than actually failing?
Watch the content-length distribution per page template. Soft blocks return a clean 200 with plausible HTML that's missing the data you want, so status-code monitoring shows everything green. A sudden shift in median response size, or a drop in extraction completeness with no rise in error rate, is the signature. Store raw snapshots and screenshots so you can confirm it by looking.
Can I share one browser profile across multiple workers to save resources?
Not concurrently. Two workers browsing the same profile at once produce conflicting cookie and localStorage writes, and the write that loses is usually the session you needed. Enforce one writer per profile at a time. Sequential handoff across machines is fine and genuinely useful — that's what portable session state is for — as long as a lock guarantees the sequence.
Where should identity assignment live — in the worker or the scheduler?
The scheduler. A random pick inside the worker gives you uneven wear, accidental concurrent use of the same profile, and no way to route around a degrading identity. Assigning from the scheduler lets you apply cooldowns, per-domain success history, geo matching and a hard concurrency cap of one — and, crucially, makes the whole layer observable and tunable.
Wrapping up
The distributed web scraping architecture best practices that matter most aren't exotic. Separate scheduling from fetching from parsing. Put rate limits in shared state. Classify failures before retrying them. Store raw pages so a broken parser is a replay, not a re-fetch. Circuit-break per domain. And treat browser identity as a real architectural layer with a lifecycle, not a string you rotate.
That last point is where most otherwise-competent pipelines quietly fail. You can build an immaculate queue topology and still lose every logged-in session because the fingerprints all looked alike and the cookie jars didn't survive a deploy.
If that identity layer is the piece you're missing, Dual Login is built to be it: isolated profiles with native, internally consistent fingerprints, persistent per-profile data directories, per-profile proxy binding with geo-matched timezone and language, portable sessions that follow a profile between machines, and a local HTTP API so your scheduler drives it directly. Spin up a handful of profiles, point your existing workers at them, and see what your block rate does over a week. That's the only benchmark that counts.