Web Crawler — Architecture¶
9. Simplest architecture (then we scale it)¶
Start with the crawl loop as a single pipeline, then distribute it.
Seeds
|
[ URL Frontier ] ◄─────────────── new links (deduped)
| next() ▲
▼ │
[ Fetcher ] ──http──► the web │ extract + normalize + dedup
| (robots + DNS + politeness) │
▼ │
[ Parser / Extractor ] ───────────────────┘
|
▼
[ Content Store ] (object storage + metadata)
The crawl loop: frontier hands out a URL → fetcher downloads it (after checking robots.txt, resolving DNS,
respecting politeness) → parser extracts links + content → new links are normalized and dedup-checked → unseen
ones go back into the frontier → content is stored. Repeat forever.
Every box justified: - URL Frontier — the prioritized, polite queue of what to crawl next. The heart of the system. - Fetcher — I/O-bound downloader; many concurrent connections; enforces politeness + robots + DNS. - Parser/Extractor — pulls links + text, computes content hash, normalizes URLs. - Content Store — durable home for pages (object storage) + metadata.
The full distributed architecture (what you present)¶
┌──────────────────────────────────────────┐
Seeds ─────────► │ URL FRONTIER │
│ front queues (priority) → back queues │◄──── new deduped URLs
│ (per-host, politeness) ; sharded by host │
└──────────────┬───────────────────────────┘
│ next() (host-affinity)
┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
[ Fetcher pool ] [ Fetcher pool ] [ Fetcher pool ] (async I/O, many hosts each)
│ uses: DNS cache • robots.txt cache • per-host rate limiter
▼
[ Parser / Extractor pool ]
│ extract links → normalize → SEEN-SET check (Bloom + KV)
│ compute content hash → CONTENT-DEDUP check (SimHash)
├──────────────► Content Store: raw HTML → Object Storage (S3, compressed)
│ metadata → sharded KV
└──────────────► unseen, in-scope links → back to FRONTIER
(+ link edges → graph store, optional)
- Frontier is sharded (usually by host) so a host's URLs and its politeness state live together.
- Fetchers pull from the frontier with host affinity, run thousands of async connections, and consult the DNS cache, robots cache, and a per-host rate limiter before each request.
- Parsers dedup URLs (Bloom+KV) and content (hash/SimHash), store pages to object storage, and feed new URLs back.
- Components are stateless workers around shared, sharded state (frontier, seen-set, stores) → scale each independently.
10. Request / data flows¶
Main crawl flow¶
1. Frontier.next() → URL (highest priority host that is politeness-eligible now)
2. Fetcher: robots.allowed(url)? (cached) → if disallowed, drop
3. Fetcher: DNS.resolve(host) (cached) → connect → GET (conditional if we have etag/last-modified)
4. On 200: pass body to Parser; on 3xx: enqueue redirect target; on 4xx/5xx: record + backoff policy
5. Parser: extract links, normalize each
6. For each link: SeenSet.seen()? → if new AND in-scope AND robots-allowed → Frontier.add()
7. Parser: contentHash → ContentStore only if not a known duplicate
8. Update metadata (fetched_at, status, etag); schedule recrawl if applicable
Politeness path (per host)¶
Before fetch: check per-host limiter (≤1 in-flight / respect crawl-delay).
If not eligible yet → the host's back-queue holds its URLs until next_eligible_at.
Failure path (examples)¶
Fetch timeout/5xx → retry with backoff up to N, then park/drop the URL
Fetcher crash → in-flight URLs (leased from frontier) become visible again → re-fetched (idempotent: dedup handles it)
11. Bottlenecks (ranked)¶
| Rank | Bottleneck | Why | Detect via | Fix |
|---|---|---|---|---|
| 1 | Network bandwidth / fetch concurrency | It's I/O-bound at 100 KB × 400/s+ | throughput, saturated NICs, fetch latency | more fetchers, async I/O, distribute across regions/links |
| 2 | DNS resolution | one lookup per host, on the hot path | DNS latency, resolver QPS | DNS cache + async resolver + local resolvers |
| 3 | Frontier throughput / contention | every crawl step reads+writes it | frontier op latency, queue depth | shard by host, disk-backed, batch ops |
| 4 | Seen-set lookups | billions of checks | dedup latency | Bloom filter in memory + sharded KV backing |
| 5 | Storage write throughput | 40 MB/s+ of pages | write latency, S3 throttling | object storage (scales), compress, batch/multipart |
| 6 | Politeness ceiling (per host) | can't exceed a host's tolerance | per-host 429/blocks | crawl breadth across many hosts, not depth on few |
Note the profile: it's about throughput and coordination, not read latency. Politeness is a hard ceiling you design around (breadth), not something you "scale away."
12. Scaling each bottleneck¶
- Throughput → add fetcher machines, each running thousands of async connections; distribute across network egress points/regions. Fetchers are stateless → scale horizontally.
- DNS → aggressive caching (per host, TTL'd) + async resolution + run your own caching resolvers to avoid hammering upstream DNS.
- Frontier → shard by host across nodes; disk-backed queues; keep politeness state local to each shard.
- Seen-set → Bloom filter (memory-efficient) fronting a sharded KV; shard by
hash(url). - Storage → object storage (effectively infinite), compress on write, batch/multipart uploads; tier old data.
- Politeness → maximize global throughput by spreading load over many hosts simultaneously; you cannot exceed one host's rate, so coverage comes from breadth + prioritization.
20. Evolution with scale¶
| Stage | Shape | What forced the jump |
|---|---|---|
| 1 · Small | single-process loop: in-memory frontier + set, fetch, store to disk | — |
| 2 · Moderate | fetcher/parser worker pools + durable frontier + Bloom seen-set + object storage | one process can't hit throughput; crash lost the frontier |
| 3 · Large | frontier sharded by host + fetchers with host affinity + DNS/robots caches + content dedup | frontier contention; DNS bottleneck; duplicate content |
| 4 · Extreme | multi-region fetchers, prioritized + freshness-aware recrawl scheduler, JS-render tier for select sites | bandwidth/geography limits; freshness needs; SPA sites |
The transitions are driven by throughput, coordination, and dedup at scale — very different triggers from a read-heavy service. Multi-region here is about bandwidth/egress and crawling geographically-close sites, not user latency.
→ Next: Deep Dives