Design a Web Crawler
The crawler fetches about 400 pages a second without overloading any one site, and most links it finds point at pages it has already seen. The design rests on a URL frontier that enforces politeness per host and cheap checks that skip duplicate URLs and duplicate content.
1 · Requirements
The Prompt
Crawl 1B web pages a month for a search index. Respect robots.txt and per-site limits, skip duplicate pages, and recrawl pages as they change.
Functional
- Crawl pages from a seed list and follow the links they contain
- Respect robots.txt and a request limit for each host
- Skip URLs already crawled and pages that duplicate stored content
- Recrawl pages on a schedule that matches how often they change
- Store raw pages for the indexer
Non-Functional
- Sustains about 400 pages a second, with headroom for recrawls
- Sends any one host at most about one request a second
- A crashed worker loses no URLs, and the crawl resumes where it stopped
- Survives spider traps, malformed HTML, and slow servers
2 · Estimates
Drill This Phase First
Show the Answer and Estimates
Answer
About 400 a second
1B divided by 30 days is about 33M pages a day. 33M divided by 86,400 seconds is about 385, so plan for 400 a second.
- Pages
- 1B pages a month: about 33M a day, or about 400 a second
- Storage
- About 500 KB a page: 500 TB a month, and about 30 PB over five years in object storage
- Bandwidth
- 400 pages a second at 500 KB is about 200 MB a second, roughly 1.6 Gbps inbound
- Fetchers
- Each fetch waits about 1 second on the network, so about 400 stay in flight. Async workers holding 100 connections each need 4 nodes, and 10 leave room for recrawls
- Seen URLs
- About 5B known URLs in a Bloom filter at 1% false positives: about 10 bits each, roughly 6 GB in memory
- Politeness
- At one request a second per host, a 1M page site takes about 12 days, so large hosts get a crawl budget of their own
3 · API
POST /seeds { urls[], priority } → 202 (adds URLs to the frontier)
GET /urls/:url_hash → { status, last_crawled_at, next_crawl_at, content_hash }
PUT /hosts/:host/policy { max_rps, paused } → 204 (overrides the default politeness limit)
Event: page.fetched { url, fetched_at, blob_key, content_hash } → indexer topicData Model
urls (Cassandra, partition = url_hash):
url_hash (PK) : bytes, 8 bytes of SHA-1(normalized url)
url : string
host : string
status : enum(pending|fetched|failed|blocked)
last_crawled_at : timestamp?
next_crawl_at : timestamp from the page's change rate
content_hash : bytes? SimHash of the last fetch
hosts (Redis, key = "host:{host}"):
robots_rules : string cached 24h
next_allowed_at : timestamp politeness gate
crawl_delay_ms : int
pages (object storage, key = date/url_hash):
raw HTML, gzip4 · High-Level Design
5 · Deep Dives
The URL Frontier
Split the frontier into two layers. Front queues order URLs by priority, from link rank or how often the page changes. Back queues hold one host each, and a worker takes from a back queue only once that host's next allowed time has passed. Map hosts to workers with consistent hashing, so one worker owns each host and politeness needs no coordination.
Duplicate URLs and Duplicate Content
Normalize each URL first: lowercase the host, drop fragments, and strip tracking parameters. Then check the Bloom filter. A false positive skips a new page about 1% of the time, a cost a crawler can accept. Mirrors and boilerplate make many pages near-copies, so fingerprint each page with SimHash and drop any within a few bits of one already stored.
Traps and Failures
Cap URL depth and length per host to escape spider traps such as endless calendar pages. Time out slow servers after a few seconds and retry with backoff. The frontier is persisted, and a worker acknowledges a URL only after its page is stored, so a crashed fetcher's URLs return to the queue.
6 · Wrap Up
The Short Version
A URL frontier with per-host queues enforces politeness. Fetchers resolve DNS through a local cache and store raw HTML in object storage. A Bloom filter skips seen URLs, and a SimHash fingerprint skips duplicate content.