Skip to content

πŸ•·οΈ Web Crawler β€” High-Level Design

Target Level: Senior/Staff Engineer Focus: Concurrent crawling, politeness, deduplication, graceful shutdown


1. SYSTEM OVERVIEW

Purpose: Crawl web pages to index content, extract links, and build a searchable corpus.

Scale: this LLD is a single process. The distributed target is 1 B pages/month: β‰ˆ 400 pages/s on average. At about 100 KB of HTML per page that's β‰ˆ 40 MB/s (β‰ˆ 320 Mbit/s) of ingress and β‰ˆ 100 TB/month of raw HTML before compression. Each crawler node handles roughly 50–200 pages/s, so plan for about 5–10 nodes with headroom.


2. SYSTEM ARCHITECTURE

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Crawl(ctx, seeds)                        β”‚
β”‚                                                                 β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   jobs (unbuffered)          β”‚
β”‚   β”‚ Coordinator goroutine        β”‚ ─────────────────► β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚   β”‚  frontier (FIFO = BFS)       β”‚                    β”‚Worker β”‚ β”‚
β”‚   β”‚  seen-set (normalized URLs)  β”‚ ◄───────────────── β”‚ Γ— N   β”‚ β”‚
β”‚   β”‚  inFlight / dispatched       β”‚   results          β””β”€β”€β”€β”¬β”€β”€β”€β”˜ β”‚
β”‚   β”‚  filters, depth, MaxPages    β”‚                        β”‚     β”‚
β”‚   β”‚  OnPage callback (serial)    β”‚                        β–Ό     β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     hostTable: robots (Once) β”‚
β”‚                                        + per-host waitTurn      β”‚
β”‚          crawlGroup: first error cancels ctx; Wait joins all    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                                β–Ό
                     Fetcher (HTTPFetcher | FakeWeb)

Figure: one coordinator owns the state; workers only do I/O through unbuffered channels.

flowchart LR
  C["Coordinator: frontier, seen-set, counters"] -->|"jobs"| W["Workers x N"]
  W -->|"results"| C
  W --> H["hostTable: robots, per-host turn"]
  W --> F["Fetcher (HTTP or fake)"]

3. CRAWL FLOW

1. Seeds β†’ Normalize β†’ filters/depth β†’ seen-set β†’ frontier
2. Coordinator dispatches the frontier head to an idle worker (if within MaxPages)
3. Worker: robots.txt (once per host) β†’ wait for the host's turn β†’ fetch (timeout, body limit)
4. Worker extracts and resolves links from 2xx text/html β†’ sends a result
5. Coordinator: update stats, admit new links (normalize β†’ filter β†’ depth β†’ seen), call OnPage
6. Stop when the frontier is empty AND inFlight == 0, or the budget is spent, or ctx is done, or OnPage returns an error

Figure: crawl flow, with admission of new links at the coordinator.

flowchart TD
  S["Seeds"] --> N["Normalize, filter, depth, seen-set"]
  N --> FR["Frontier (FIFO)"]
  FR --> D["Coordinator dispatches to idle worker"]
  D --> R["Worker: robots.txt, host turn, fetch"]
  R --> L["Extract links from 2xx HTML"]
  L --> RES["Result to coordinator: stats, OnPage"]
  RES --> N
  RES --> T{"Frontier empty and inFlight = 0?"}
  T -- Yes --> DONE[Stop]

4. POLITENESS STRATEGY

Concern Implementation
robots.txt Fetched once per host per crawl; RFC 9309 groups, longest match, */$; 4xx β†’ allow all, 5xx/unreachable β†’ disallow all
Rate limiting Minimum gap per host = max(PerHostDelay, Crawl-delay), claimed under the host lock after a ctx-aware wait
Crawl delay Honoured (non-standard; Bing uses it, Google ignores it)
Concurrent requests per host Not capped separately here; add a per-host semaphore (production: 1–2)
Identification Explicit User-Agent

5. CONCURRENCY MODEL

Pattern Implementation
Single owner Coordinator owns frontier, seen-set and counters, so they need no locks
Worker pool N goroutines doing only I/O
Fan-out / fan-in Unbuffered jobs β†’ workers β†’ unbuffered results; the coordinator selects on both (nil-channel trick)
Channel ownership The coordinator closes jobs; results is never closed; every send also selects on ctx.Done()
errgroup crawlGroup: first error cancels; Wait joins every goroutine before Crawl returns
Shared state Only hostTable (mutex) and hostState (sync.Once + mutex)

6. TRADE-OFF ANALYSIS

Decision Choice Rationale
Deduplication Plain map owned by the coordinator The check and the insert are one step; no sync.Map races
URL frontier In-memory FIFO slice BFS order; production uses Mercator front/back queues on disk
Termination Frontier empty AND inFlight == 0 Exact; no polling or timeouts
Politeness Per-host turn + robots Avoids overwhelming servers and respects the site's rules
API Synchronous Crawl + OnPage No channel the caller must drain to avoid leaks
Extraction Regex over <a href> Stdlib-only; production uses an HTML tokenizer (x/net/html)
Normalization Conservative (keeps path case and trailing slash) Merging distinct URLs loses pages; extra fetches only cost time

7. FAILURE MODES

Failure Effect Mitigation
Spider trap (calendar, infinite params) Unbounded frontier Depth limit, per-host page budget, max URL length, repeated-segment detection
Host slow or down Workers tied up; politeness delays pile up Fetch timeout; error-rate circuit breaker per host; Mercator back queues to avoid head-of-line blocking
429 / 503 storms Getting banned Honour Retry-After, exponential backoff per host, lower that host's rate
robots.txt unreachable Unknown permissions Treat as full disallow (RFC 9309); retry later
Huge or binary responses Memory blow-up io.LimitReader, Content-Type check before parsing
Crawler crash Lost frontier Checkpoint frontier + seen-set (RocksDB); refetch since the checkpoint (at-least-once)
Seen-set too big for RAM OOM Bloom filter (β‰ˆ 1.2 GB per 1 B URLs at 1% FP) in front of a disk set
Duplicate content under different URLs Wasted crawl budget rel=canonical, redirect targets, SimHash near-dup detection