Design a Web Crawler
DifficultyHard | HelloInterview: problem breakdown
Problem Statement
Design a large-scale web crawler that systematically downloads and indexes web content. Used by search engines (Googlebot), SEO tools, archiving services (Wayback Machine), and security scanners.
๐Real-world: The original cautionary tale is the Morris Worm (1988) โ arguably the first internet "crawler" gone wrong: a bug in how it tracked already-visited hosts made it re-infect machines repeatedly, crippling ~10% of the internet and producing the first CFAA conviction. It's the perfect illustration of why this design's hard parts are politeness (don't hammer one domain โ respect
robots.txtand rate-limit per host) and deduplication at scale (a Bloom filter for "have I seen this URL?", because tracking billions of URLs exactly is infeasible). Security angles worth raising: crawlers must avoid crawler traps (infinite calendars, dynamically generated link mazes), beware SSRF-style pitfalls (a malicious page linking tohttp://169.254.169.254/or internal IPs โ your crawler shouldn't fetch cloud metadata or internal services), and handle that most of the modern web is hostile or adversarial (cloaking, spam farms). Note the same design powers security scanners โ a vuln scanner is a crawler with payloads.
Requirements
Functional
- Crawl seed URLs and recursively follow links
- Respect
robots.txt(politeness) - Avoid re-crawling unchanged pages (freshness)
- Store crawled content for downstream processing
- Handle redirects, different MIME types, duplicate content
Non-Functional
- Crawl 1B pages/month โ ~400 pages/s
- Crawl budget: don't overwhelm any single host (max 1 req/s per domain)
- Detect and avoid infinite crawl loops
- Scale horizontally across many crawler nodes
Core Design
URL Frontier (Priority Queue)
The URL frontier is the queue of URLs to crawl. It has two tiers:
(priority): ordered by priority (importance of domain, freshness)
(politeness): one queue per host, enforcing crawl delay
Scheduler logic:
for each crawler worker:
pick next available host queue that hasn't been polled in last N seconds
dequeue one URL from that host's queue
dispatch to crawler workerThis enforces one request per host per N seconds without blocking all workers.
Deduplication
Billions of URLs; many are equivalent or already crawled.
URL normalization
- Remove default ports (
http://example.com:80/โhttp://example.com/) - Lowercase host
- Remove
#fragment(fragment is client-side only) - Sort query parameters consistently
Seen URL check (bloom filter):
bloom_filter.add(normalized_url)
if bloom_filter.contains(url): skip # probably seen (false positives acceptable)- Bloom filter for 10B URLs: ~10 GB memory (acceptable)
- False positive rate ~0.1% โ occasionally re-crawl a URL (acceptable)
Content deduplication (SimHash):
- After downloading, compute SimHash of content
- Two pages with SimHash Hamming distance < 3 โ near-duplicates (mirror content)
- Store SimHash of each page; skip near-duplicates
robots.txt
Every crawler must respect robots.txt:
User-agent: *
Disallow: /admin/
Crawl-delay: 10
User-agent: Googlebot
Allow: /- Fetch and cache
robots.txtper domain on first visit - Cache TTL: 24h (re-fetch regularly in case it changes)
- Respect
Crawl-delaydirective
Architecture
Seed URLs โ URL Frontier (Redis sorted set + per-domain queues)
โ
Crawler Workers (auto-scaling)
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DNS Resolver (cached, rate-limited)โ
โ HTTP Client (timeout, redirect cap)โ
โ robots.txt Cache โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
Raw Content Store (S3 / HDFS)
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Link Extractor โ โ new URLs โ URL Frontier
โ Content Parser (HTML, PDF...) โ โ Document Index (Elasticsearch)
โ SimHash Dedup โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
URL Metadata DB (Postgres):
- last_crawled_at
- content_hash
- crawl_status
- next_crawl_atRecrawl Scheduling (Freshness)
Not all pages change at the same rate. Schedule recrawls based on change frequency:
- News sites: every 15 minutes
- Product pages: daily
- Static documentation: weekly
Track last_modified header and ETag. Send If-Modified-Since / If-None-Match on recrawl โ server returns 304 Not Modified if unchanged โ skip processing.
Key Design Decisions
| Decision | Choice | Reason |
|---|---|---|
| URL dedup | Bloom filter | Memory-efficient; false positive rate acceptable |
| Content dedup | SimHash | Detects near-duplicates; single-pass computation |
| URL priority | Sorted set (Redis ZADD) | O(log N) insert/extract; flexible priority |
| Politeness | Per-domain queue + delay | Avoid overloading any single host |
| Storage | S3 for raw HTML | Cheap, durable, decoupled from processing |
| Crawl policy | BFS with priority adjustments | Explore breadth first; boost important domains |
Security Considerations
| Threat | Mitigation |
|---|---|
| Crawler traps (infinite links) | Max depth limit; max URLs per domain; detect URL patterns |
| Malicious redirects to internal IPs (SSRF) | Block RFC 1918 IP ranges; validate resolved IP before connecting |
| Poisoned robots.txt (Disallow: /) | Respect but audit; alert on suspiciously broad disallow |
| Zip bomb / gigantic responses | Max response size limit (e.g., 5MB); timeout on download |
| Malware in crawled content | Don't execute scripts; sandboxed rendering for JavaScript |
| Legal: crawling private content | Honor noindex meta tags; respect login-wall boundaries |
| Rate limiting by target sites | Respect Retry-After; respect Crawl-delay; don't try to bypass |
Interview Tips
always mention this first. Ignoring it is both technically wrong and legally problematic.
URL dedup (bloom filter) and content dedup (SimHash) โ distinguish them.
(e.g., ?page=1&page=2&page=3...) โ set max URL depth and max URLs per domain.
when a crawler resolves a hostname, an attacker's DNS can return an internal IP. Validate IP after DNS resolution, before connecting.