Designing Distributed Crawlers for Billion-Page Datasets: Architecture, Scheduling, and Fault Tolerance

Engineering guide ยท Last reviewed 8 October 2026 ยท 10 min read

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 problem

Once 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.

Figure 1 โ€” end-to-end crawler pipeline
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 next

A 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:

Example 1 โ€” an in-memory frontier (Python)
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 URLs

A 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.

PartitionHosts
Partition 0example.com, shop.example.com, api.example.com
Partition 1news-site.com, media.news-site.com
Partition 2ecommerce.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:

Example 2 โ€” per-host scheduling state (Python)
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 pace

Suppose 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.

Example 3 โ€” scaling workers from queue depth (Python)
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 times

At 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.

Figure 2 โ€” Bloom filter membership test
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:

ComponentRole
Bloom filterFast rejection of URLs that have probably been seen
Distributed key-value storeAuthoritative state
Canonical URL storeThe normalized form of each URL

URL Canonicalization Is More Important Than It Looks

URL identity versus resource identity

Consider three URLs:

Figure 3 โ€” three URLs, or one resource?
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.

Example 4 โ€” a basic URL normalizer (Python)
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 condition

At 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:

Figure 4 โ€” acknowledge last
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.

Example 5 โ€” an idempotent write (SQL)
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 stores

A 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.

DataWhere it livesExamples
Raw HTMLObject storageRaw documents
MetadataSQL or NoSQL storePostgreSQL or another distributed database
MetricsTime-series databaseCrawl and latency metrics
Analytical workloadsAnalytical systemsClickHouse, 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 path

At 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.

Figure 5 โ€” network abstraction
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 job

One architectural extension is to make network selection part of the scheduler. The fetch call states what the job needs:

Example 6 โ€” a fetch call with a connection profile (Python)
fetch(
    url=url,
    region="US",
    network_type="residential"
)

And the job itself carries those requirements:

Example 7 โ€” network requirements on the crawl job (Python)
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 slow

At billion-page scale, “the crawler is slow” is not an actionable diagnosis. Engineering teams need to know where the system is slow.

LayerSignal to watchUseful metrics
SchedulerQueue lagqueue_depth, consumer_lag
NetworkLatency and errorsfetch_latency_seconds, crawl_failure_total, retry_rate
StorageWrite latencystorage_latency_seconds
OverallVolume and qualitycrawl_urls_total, crawl_success_total, duplicate_rate, data_freshness_seconds

Instrumenting a request path takes only a few lines with a metrics client:

Example 8 โ€” request counter and latency histogram (Python)
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 dataset

A 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.

Example 9 โ€” recrawl interval from change rate (Python)
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 rateRecrawl interval
Above 0.83,600 seconds (1 hour)
Above 0.321,600 seconds (6 hours)
0.3 or below86,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 part

It 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.

Figure 6 โ€” control plane and data plane
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 checklist

The 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 choiceWhat it gives you
Durable URL frontierWorker failures do not become data loss
PartitioningHorizontal scaling
BackpressureDownstream systems are protected
Probabilistic deduplicationState growth stays under control
Idempotent processingRetries are safe
Object storageRaw data is separated from transactional metadata
ObservabilityFailures become measurable events
Adaptive schedulingBetter freshness without wasted compute
Network abstractionConnectivity 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 infrastructure

A 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 short
What 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.

Flexible Pricing Plan

logo purple proxyempire

Our state-of-the-art proxies.

Experience online freedom with our unrivaled web proxy solutions. Pioneering in collecting location specific data at scale, our premium, ethically-sourced network boasts a vast pool of IPs, expansive location choices, high success rate, and versatile pricing. Advance your digital journey with us.

๐Ÿ˜๏ธ Rotating Residential Proxies
  • 30M+ Premium Residential IPs
  • ย 170+ Countries
    Every residential IP in our network corresponds to an actual desktop device with a precise geographical location. Our residential proxies are fast and reliable, with 99.9% uptime, and work for a wide range of use cases. You can use Country, Region, City and ISP targeting for our rotating residential proxies.

See our Rotating Residential Proxies

๐Ÿ“ Static Residential Proxies
  • 24 Countries
    Buy a dedicated static residential IP from one of the 24 countries that we offer proxies in. Keep the same IP for a month or longer, while benefiting from their fast speed and stability.

See our Static Residential Proxies

๐Ÿ“ณ Rotating Mobile Proxies
  • 4M+ Premium Mobile IPs
  • ย 170+ Countries
    Access millions of clean mobile IPs with precise targeting including Country, Region, City, and Mobile Carrier. Get far fewer IP blocks and CAPTCHAs with our 4G and 5G proxies.

See our Mobile Proxies

๐Ÿ“ฑ Dedicated Mobile Proxies
  • 5+ Countries
  • 50+ Locations
    Get your own dedicated mobile proxy in one of our supported locations, with unlimited bandwidth and unlimited IP changes on demand. A great choice when you need a small number of mobile IPs and a lot of proxy bandwidth.

See our 4G & 5G Proxies

๐ŸŒ Rotating Datacenter Proxies
  • 197,000+ IPs Premium IPs
  • ย 62 Countries
    On a budget and need to do some simple scraping tasks? Our datacenter proxies are the perfect fit! Get started with as little as $2

See our Datacenter Proxies

proxy locations

30M+ rotating IPs

99% uptime - high speed

99.9% uptime.

dedicated support team

24/7 Dedicated Support.

fair price

Fair Pricing.

๐Ÿ  Residential Proxies Rotating / Static / Unlimited
๐Ÿ“ฑ Mobile Proxies Rotating and Dedicated
๐Ÿ–ฅ๏ธ Datacenter Proxies Rotating
๐ŸŒ IP Pool 30M+ residential + 4M+ mobile IPs
๐Ÿ“ถ Uptime 99.9% ยท Live status
๐Ÿ’ณ Payment Card ยท PayPal ยท Crypto ยท Bank transfer
๐Ÿ’ฌ Support 24/7 live chat ยท [email protected]