A distributed crawler rarely fails because a single HTTP request is hard to execute. It fails because billions of requests create a distributed-systems problem. This guide walks through the architecture of a crawler built for billion-page datasets: the URL frontier, partitioning, backpressure, deduplication, fault tolerance, storage, the network layer, observability and freshness, with short Python and SQL examples for each idea.
The short version
Design the URL frontier and the scheduler first, and treat workers as disposable. Partition work by host, let downstream capacity throttle upstream discovery, deduplicate with shared state, make every write idempotent, split storage by data lifecycle, and keep the network layer behind an abstraction the scheduler can select from.
About the code in this guide. The snippets are deliberately small. Each one illustrates a single design idea and is not production code. The thresholds and intervals in them are example values to tune against your own workload.
Key takeaways
- At billion-page scale the URL frontier matters more than the fetching workers. The scheduler must outlive any worker that runs its jobs.
- Partition by hostname or domain so work scales horizontally and politeness policies stay per-domain.
- Use queue depth and consumer lag as control signals. Scale on system pressure, not a fixed worker count.
- Deduplication needs shared state: a Bloom filter for fast rejection, backed by an authoritative store.
- Assume at-least-once execution and make writes idempotent so retries cannot corrupt the dataset.
- Make region and network type part of the crawl job, so proxy logic stays out of application code.
From Scraper to Distributed Data Platform
Why scale changes the problemOnce a crawler grows beyond a small collection of workers, almost every component becomes stateful. URLs need to be scheduled, deduplicated, prioritized, retried, partitioned, stored and eventually revisited. Workers fail. Networks become inconsistent. Domains respond at different speeds. Data changes continuously.
At that point, “scraping” is no longer an appropriate architectural description. You are building a distributed data acquisition platform. If the distinction between the two is new, our comparison of web crawlers and web scrapers covers the basics.
URL Sources
|
v
URL Frontier
|
v
Scheduling Layer
|
v
Worker Cluster / Priority Queue
|
v
Fetching Layer
|
v
Raw Data Store + Metadata Store
|
v
Processing Pipeline
|
v
Search / AI / BI
The interesting engineering problem is not how to download a page. It is how to coordinate this entire system while maintaining throughput, correctness, freshness and predictable infrastructure costs.
Start With the URL Frontier, Not the Worker
The component that decides what happens nextA common architectural mistake is to start by optimizing the fetching workers. At billion-page scale, the URL frontier is often more important.
The frontier is the distributed system responsible for one decision: what should we fetch next? That decision can depend on priority, domain, freshness, crawl history, discovered links, business importance, previous failures and scheduling constraints.
The prototype version of a frontier is an in-memory queue:
from collections import deque
queue = deque()
queue.append("https://example.com/page")
url = queue.popleft()
This works for a prototype. It fails as soon as multiple workers, processes, machines and failure domains become involved. If the worker disappears, the queue disappears with it.
A production crawler therefore separates URL discovery from URL execution. A durable messaging system such as Kafka can provide the foundation for this architecture, but the important principle is broader:
The scheduler should not depend on the lifetime of the worker executing the job.
Partitioning the Web
One queue cannot hold a billion URLsA billion URLs cannot realistically be treated as one queue. The system needs a partitioning strategy, and one common approach is partitioning by hostname or domain.
| Partition | Hosts |
|---|---|
| Partition 0 | example.com, shop.example.com, api.example.com |
| Partition 1 | news-site.com, media.news-site.com |
| Partition 2 | ecommerce.com, cdn.ecommerce.com |
Partitioning provides two important properties:
- Horizontal scalability. Work can be spread across machines.
- Domain-level scheduling policies. The system can control how each site is crawled.
A crawler should not interpret “1,000 workers” as permission to send 1,000 simultaneous requests to the same website. Large-scale crawling requires politeness and rate control as first-class architectural concepts. A minimal per-host state might look like this:
from dataclasses import dataclass
from time import monotonic
@dataclass
class HostState:
next_allowed: float = 0
failures: int = 0
def can_schedule(host: HostState) -> bool:
return monotonic() >= host.next_allowed
Two different resources. Fetching capacity and scheduling capacity are not the same thing. Having spare workers does not mean a given host can accept more requests.
Backpressure Is What Keeps the System Alive
Let downstream capacity set the paceSuppose your crawler discovers 50,000 URLs per second but your processing infrastructure can only consume 30,000 URLs per second. The queue grows continuously, increasing memory, storage, latency and recovery time.
The answer is backpressure: a healthy distributed system allows downstream capacity to influence upstream production. Consumer lag can therefore become an operational control signal.
def adjust_workers(queue_depth, worker_count, max_workers):
if queue_depth > 1_000_000:
return min(worker_count + 10, max_workers)
if queue_depth < 10_000:
return max(worker_count - 5, 1)
return worker_count
Scale according to system pressure, not according to a fixed worker count. The same principle applies to browser-based workloads, as covered in our guide to designing a browser cluster for 10,000 concurrent Chromium sessions.
Deduplication Becomes a Distributed Data Problem
The same URL arrives many timesAt billion-page scale, URL duplication becomes extremely expensive. The same URL may appear:
- on multiple pages;
- through different discovery paths;
- with tracking parameters;
- in multiple sitemap files;
- across repeated crawl cycles.
A naive Python set cannot survive distributed execution. The state must be shared or partitioned. Bloom filters are particularly useful for large-scale probabilistic membership testing.
URL -> Hash Functions -> Bit Array
|-> probably seen
|-> definitely not seen
A Bloom filter does not store the entire URL. It stores a probabilistic representation, which dramatically reduces memory requirements at the cost of a controllable false-positive rate.
A production system can combine three components:
| Component | Role |
|---|---|
| Bloom filter | Fast rejection of URLs that have probably been seen |
| Distributed key-value store | Authoritative state |
| Canonical URL store | The normalized form of each URL |
URL Canonicalization Is More Important Than It Looks
URL identity versus resource identityConsider three URLs:
https://example.com/product?id=123
https://example.com/product?id=123&utm_source=google
https://example.com/product/?id=123
Are these three resources or one? There is no universal answer. Canonicalization is therefore not simply string manipulation. It is part of the crawler’s data model.
from urllib.parse import urlsplit, urlunsplit
def normalize(url):
parts = urlsplit(url)
return urlunsplit((
parts.scheme.lower(),
parts.netloc.lower(),
parts.path.rstrip("/") or "/",
"",
""
))
This example drops the query string entirely. Production systems need significantly more sophisticated rules, particularly when query parameters affect content. In the URLs above, id=123 identifies the product, so stripping it would merge different pages.
Crawler correctness depends on understanding the difference between URL identity and resource identity.
Fault Tolerance: Assume Every Worker Will Fail
Failure is a normal operating conditionAt sufficient scale, failures are normal operating conditions. A worker can disappear because of hardware failure, container termination, out-of-memory conditions, network errors, deployment or dependency failure.
The order of operations inside a worker is what makes that survivable:
Receive -> Process -> Persist Result -> Acknowledge
A worker should not acknowledge a job simply because it received it. If the worker fails between processing and acknowledgement, the system may process the job again.
Design for at-least-once execution
Instead of assuming that every job executes exactly once, make the operation idempotent. Retries should not corrupt the dataset.
INSERT INTO pages(url, content_hash)
VALUES (%s, %s)
ON CONFLICT (url)
DO UPDATE SET content_hash = EXCLUDED.content_hash
Storage Architecture Should Follow the Data Lifecycle
Different data, different storesA billion-page crawler generates enormous quantities of information. Trying to put everything into one relational database is usually the wrong abstraction. Each crawl event produces several kinds of data, and each belongs somewhere different.
| Data | Where it lives | Examples |
|---|---|---|
| Raw HTML | Object storage | Raw documents |
| Metadata | SQL or NoSQL store | PostgreSQL or another distributed database |
| Metrics | Time-series database | Crawl and latency metrics |
| Analytical workloads | Analytical systems | ClickHouse, BigQuery or Snowflake |
Do not make your transactional database carry your analytical workload.
The Network Layer Is Part of the Architecture
A healthy worker on an unhealthy pathAt scale, networking becomes one of the largest variables in crawler performance. A worker may be healthy while the network path experiences latency variation, regional differences, connection failures, routing changes or bandwidth constraints.
For legitimate public-data collection, localization testing, market intelligence, price monitoring, SEO monitoring and similar workloads, ProxyEmpire can sit between the fetching layer and the public internet. Its infrastructure includes residential, mobile, static residential (ISP) and datacenter connectivity, with location targeting and common proxy protocols.
Crawler
|
Network Abstraction
|
Proxy Layer
|
Internet
The crawler should not need to know how individual network endpoints are sourced or managed. It should request a connection profile appropriate for the workload. This keeps infrastructure replaceable and reduces coupling between application code and network topology.
Why Proxy Selection Should Be a Scheduling Decision
Region and network type belong on the jobOne architectural extension is to make network selection part of the scheduler. The fetch call states what the job needs:
fetch(
url=url,
region="US",
network_type="residential"
)
And the job itself carries those requirements:
class CrawlJob:
def __init__(self, url, region=None, network_type="default"):
self.url = url
self.region = region
self.network_type = network_type
This design allows the same crawler to support different legitimate requirements without embedding proxy logic throughout the codebase. A global localization dataset may require region-specific access, while a high-volume dataset that is not location-sensitive may prioritize cost and latency.
Observability Is the Difference Between Scaling and Guessing
Know where the system is slowAt billion-page scale, “the crawler is slow” is not an actionable diagnosis. Engineering teams need to know where the system is slow.
| Layer | Signal to watch | Useful metrics |
|---|---|---|
| Scheduler | Queue lag | queue_depth, consumer_lag |
| Network | Latency and errors | fetch_latency_seconds, crawl_failure_total, retry_rate |
| Storage | Write latency | storage_latency_seconds |
| Overall | Volume and quality | crawl_urls_total, crawl_success_total, duplicate_rate, data_freshness_seconds |
Instrumenting a request path takes only a few lines with a metrics client:
from prometheus_client import Counter, Histogram
requests = Counter("crawler_requests_total", "Total crawler requests")
latency = Histogram(
"crawler_request_latency_seconds",
"Crawler request latency"
)
def record_request(duration):
requests.inc()
latency.observe(duration)
Throughput alone is a poor success metric. A better KPI is useful, fresh, validated data per unit of infrastructure cost.
Freshness Changes the Architecture
From one-off crawl to continuous datasetA crawler that runs once is relatively simple. A crawler that maintains a dataset continuously is fundamentally different. Resources that change hourly, weekly and monthly should not receive identical recrawl schedules.
def next_crawl_interval(change_rate):
if change_rate > 0.8:
return 3600
if change_rate > 0.3:
return 21600
return 86400
In this example the intervals are in seconds, so the three tiers map to the following schedule:
| Historical change rate | Recrawl interval |
|---|---|
| Above 0.8 | 3,600 seconds (1 hour) |
| Above 0.3 | 21,600 seconds (6 hours) |
| 0.3 or below | 86,400 seconds (24 hours) |
A mature crawler can assign recrawl intervals based on historical change frequency. This transforms the crawler from a downloader into a continuous data freshness engine.
The Real Scaling Problem Is Coordination
Compute is the easy partIt is tempting to think that billion-page crawling is primarily a compute problem. Compute is comparatively easy to scale horizontally. The difficult problems are coordination, state, scheduling, correctness and failure recovery.
Adding another hundred workers is easy. Ensuring those workers do not duplicate work, overload the same host, lose jobs, corrupt state or overwhelm downstream storage is considerably harder.
Control Plane
|-- Scheduler
`-- Policy
|
Message Bus
|
Worker Fleet
|
Processing Layer
|
Object Storage + Analytics
That is no longer a scraper. It is a distributed data platform.
Designing for the Next Billion Pages
A design checklistThe architecture of a large-scale crawler should be based on how the system behaves when every component is under pressure simultaneously. Each design choice in this guide answers one of those pressures.
| Design choice | What it gives you |
|---|---|
| Durable URL frontier | Worker failures do not become data loss |
| Partitioning | Horizontal scaling |
| Backpressure | Downstream systems are protected |
| Probabilistic deduplication | State growth stays under control |
| Idempotent processing | Retries are safe |
| Object storage | Raw data is separated from transactional metadata |
| Observability | Failures become measurable events |
| Adaptive scheduling | Better freshness without wasted compute |
| Network abstraction | Connectivity infrastructure stays independent from crawler logic |
ProxyEmpire can occupy that network abstraction layer for organizations collecting public data, performing global market research, monitoring prices, validating localized experiences or building distributed data infrastructure.
Conclusion: A Crawler at Scale Is a Distributed System
Engineer it like infrastructureA billion-page crawler is not a faster scraper. It is a distributed system with the same fundamental problems found in large-scale backend infrastructure: state management, scheduling, partitioning, queues, fault tolerance, observability, storage and resource optimization.
The HTTP request is merely the smallest unit of work. The real engineering challenge is coordinating millions or billions of those units while maintaining correctness and freshness.
For CTOs building AI data pipelines, competitive intelligence platforms, search infrastructure, market research systems or large-scale public-data platforms, this distinction matters. Once the crawler becomes infrastructure, it should be engineered like infrastructure.
The goal isn’t to fetch more pages. The goal is to build a system that can continuously produce reliable, fresh, structured data, even when individual components inevitably fail.
Frequently Asked Questions
Distributed crawlers, in shortWhat is a URL frontier in a web crawler?
The URL frontier is the system that decides what the crawler should fetch next. That decision can depend on priority, domain, freshness, crawl history, discovered links, business importance, previous failures and scheduling constraints. At billion-page scale it should be durable and independent of the workers that execute jobs.
Why partition a distributed crawler by hostname or domain?
Partitioning makes work horizontally scalable and lets the system keep domain-level scheduling policies. That is what allows politeness and rate control per site, so a large worker fleet does not send all of its requests to the same website at once.
How do large crawlers deduplicate URLs?
With shared or partitioned state rather than an in-process set. A common combination is a Bloom filter for fast rejection, a distributed key-value store for authoritative state and a canonical URL store. A Bloom filter keeps a probabilistic representation of each URL, which saves memory at the cost of a controllable false-positive rate.
What does backpressure mean in a crawler?
Backpressure means downstream capacity influences upstream production. If the crawler discovers URLs faster than the processing pipeline can consume them, the queue grows without bound. Using queue depth or consumer lag as a control signal lets the system scale workers up or down based on actual pressure.
How should a crawler handle worker failures?
Acknowledge a job only after its result has been persisted, accept that jobs will sometimes run more than once, and make writes idempotent so a retry cannot corrupt the dataset. An upsert keyed on the URL is a simple example.
Where do proxies fit in a distributed crawler architecture?
Between the fetching layer and the public internet, behind a network abstraction. The crawler requests a connection profile, such as a region and a network type, and the scheduler can treat that choice as part of the job. This keeps proxy logic out of application code and keeps the network infrastructure replaceable.
Put ProxyEmpire in your crawler’s network layer
Residential, mobile, static residential (ISP) and datacenter proxies with location targeting, for public-data collection, price monitoring, market research and localization testing.














