InterviewKata
System design · Worked design D09 of 46 · Social and content · Long form

Design a web crawler

Start from a few seed pages, download them, find their links, and keep going until you've fetched a billion pages, without downloading the same page twice, without hammering any one website, without getting stuck in infinite link mazes, and without losing progress when machines fail. A crawler is a giant, carefully scheduled queue, which makes it an excellent exercise in Level 4 thinking. It follows the interview framework phase by phase.

1. The prompt

"Design a web crawler that fetches a billion pages a week for a search engine (or for training data). The output is the raw HTML of each page, stored for later processing."

There are no users waiting on a response, so latency barely matters. What matters is throughput under a politeness constraint, deduplication at the scale of billions, and a pipeline that keeps going when individual machines, websites and DNS servers misbehave.

2. Frame the problem

How many pages, how fast?

About 1 billion pages a week.

~1,650 pages a second: bandwidth, concurrency and storage follow from this.

What do we keep?

Raw HTML and metadata; an indexing system reads them later.

Object storage for content, a metadata store for status and links.

How polite must we be?

Obey robots.txt, and at most about one request a second per site (configurable).

Politeness, not compute, limits throughput: we need many sites in parallel.

One crawl, or continuous?

Continuous: pages are recrawled, more often if they change often.

A scheduler that learns each page's change rate.

Do pages need JavaScript rendering?

Plain HTML for now; rendering later for pages that need it.

Rendering costs 10–100× more per page.

Which pages first?

Important ones (well linked, popular sites) before obscure ones.

The frontier needs priorities, not just FIFO.

3. What it must do

  1. The crawler fetches pages, starting from seed URLs and following extracted links.
  2. It stores each page's content and metadata for downstream indexing.
  3. It recrawls pages periodically, more often for pages that change often.

Not now:

4. How well it must do it

TargetWhy this number
Polite: obey robots.txt; never more than ~1 request a second to one site rulesImpolite crawlers get blocked, and can hurt small sites.
Throughput: 1 billion pages in 7 days load shapeThe index is only as good as its coverage and freshness.
Efficient: no URL fetched twice per crawl; duplicate content skipped load shapeEvery wasted fetch costs bandwidth and politeness budget.
Robust: survives worker crashes, bad HTML, slow servers and crawler traps, losing no progress surviving failures · data safetyA week-long job will see every kind of failure.

5. Numbers that change the design

QuantityWorking→ so we…
Fetch rate10⁹ pages ÷ (7 × 86,400 s)~1,650 pages/s; plan for ~3,000/s
Bandwidth3,000/s × ~100 KB (compressed HTML)~300 MB/s ≈ 2.4 Gbit/s → so a few dozen fetcher machines
Storage10⁹ × 100 KB~100 TB per crawl → so object storage
ConcurrencyLittle's Law: 3,000/s × ~1 s per fetch~3,000 fetches in flight → so asynchronous I/O, not a thread per request
Politeness3,000/s at 1 request/s per host→ so at least 3,000 hosts must be crawlable at every moment (try the calculator)
URLs seen~10 billion discovered URLsAn exact set would be hundreds of GB → so a Bloom filter

6. The nouns

url task   normalised_url, host, priority, depth, discovered_at
host       host, robots_rules + expiry, crawl_delay, next_allowed_at, error_count, ip (cached)
page       url, fetched_at, status, content_hash, storage_key, out_links, change_rate, next_crawl_at
content    object storage: crawl/{date}/{hash} → compressed HTML

7. The contract

No public API. Internally, components pass messages, and operators get a small admin API.

frontier.add(url, priority)                    // from parsers and the recrawl scheduler
frontier.next(node) → url task                 // a fetcher asks its node for a host that's ready
page.stored {url, content_hash, storage_key, links[]}   → parser, metadata store
admin: POST /v1/seeds {urls}  ·  POST /v1/hosts/{host}/block  ·  GET /v1/crawl/stats

Data flow: (1) seed URLs enter the frontier → (2) a fetcher takes a URL whose host is allowed now → (3) robots.txt and DNS from cache → (4) download, store raw HTML → (5) parse links, normalise, check the seen set → (6) new URLs go back to the frontier; repeat.

8. First working version

One queue of URLs, a pool of fetcher workers, a parser, and a set of seen URLs.

8.1 The crawl loop

A worker takes the next URL from the queue, downloads it, stores the HTML, extracts links, and adds every link it hasn't seen to the back of the queue.

8.2 Storing pages

Raw HTML goes to object storage under a key derived from its content hash; a metadata row records the URL, status and storage key.

8.3 Recrawling

Version one re-adds every page to the queue a week after it was fetched.

Parked for later (section 9 fixes each)

  1. Links mostly point within the same site, so the FIFO queue hammers one host.
  2. An in-memory set of 10 billion URLs doesn't fit.
  3. One page with dozens of URL spellings; mirrors with identical content.
  4. robots.txt, DNS lookups and slow servers.
  5. Infinite calendars and session-id URLs.
  6. A crashed worker loses its queue.
  7. Recrawling everything weekly wastes budget on pages that never change.

9. Make it hold up

9.1 The frontier: priority and politeness together polite · throughput

ApproachWhy
✗ BadOne big FIFO queue of URLs; fetchers take the next onePages link mostly within their own site, so the queue fills with one host's URLs; fetchers hit that host thousands of times a second and get blocked. Important pages wait behind junk.
~ GoodA queue per host, and a rate limiter per hostPolite, but there's no notion of importance, and millions of hosts means millions of queues to schedule.
✓ GreatTwo stages (the classic Mercator design): front queues by priority (importance, freshness), feeding back queues, one per host, each with a "next allowed time"; a heap of hosts ordered by next allowed time tells fetchers which host is readyImportant pages go first; each host is fetched at most at its allowed rate; fetchers are never idle while some host is ready.

Partition the frontier by hash of the host across crawler nodes, so each host's queue, rate limit and robots.txt live on exactly one node.

9.2 Don't fetch twice efficient

9.3 Robots.txt, DNS and slow servers polite · robust

9.4 Crawler traps robust · efficient

Some sites generate endless URLs: calendars with a "next month" link forever, session ids in every URL, faceted search with millions of filter combinations. Defences:

9.5 Not losing progress robust

The frontier is durable (a log such as Kafka, or a disk-backed queue per node), so a crashed node's work resumes. A fetch task is acknowledged only after the page is stored (at-least-once; the content hash makes repeats harmless). If a crawler node dies, its host partitions move to another node, which rebuilds those queues from the durable frontier.

9.6 Recrawling efficient · throughput

The web changes constantly, but not uniformly: a news homepage changes every minute, an old blog post never.

Every recrawl spends politeness budget that could discover a new page, so the split between freshness and coverage is an explicit choice.

9.7 The whole design after the deep dives

One URL through the final crawler: front queue, host queue, fetch, store, parse, and back to the frontier.

10. Interviewer follow-ups

Crawl 10 billion pages a week instead.

Scale out by host partition: more crawler nodes and fetchers. The real limits become politeness (enough distinct hosts ready at once), DNS and bandwidth. Prioritise harder, and recrawl less of what doesn't change.

Pages that need JavaScript to show content.

Detect them (HTML nearly empty, a known framework) and send only those to a pool of headless browsers, which is 10–100× more expensive per page. Cache rendered results and recrawl them less often.

A site asks you to stop crawling it.

Honour robots.txt changes at the next refresh, and offer a manual block list for immediate effect. Keep a contact page linked from the user agent.

How do you choose which pages are important?

Link-based importance (PageRank-like scores from the link graph computed by a batch job), site reputation, freshness needs, and signals from the search engine (which pages people click). The scores set front-queue priority.

The Bloom filter fills up.

False positives rise as it fills. Size it for the expected count with headroom, use a scalable Bloom filter (add a new layer when full), or rebuild it per crawl generation.

A fetcher node is in a region far from most sites.

Place fetchers in several regions and route hosts to the nearest one by IP geolocation. Fetch time drops and so does the in-flight concurrency needed.

Downstream wants new pages within an hour.

Add a fast lane: sitemaps, RSS and change notifications (where sites support them) feed a high-priority front queue for new and changed pages.

11. When things break

FailureEffectHow we detect itWhat limits the damage
Crawler node crashIts hosts pause brieflyNode heartbeatPartitions reassigned; queues rebuilt from the durable frontier
DNS resolver overloadedFetches stallLookup latencyLocal caching resolvers; per-host IP cache
Crawler trapBudget wasted on one siteURLs per host, repeated content hashesDepth and per-host caps; pattern detection
Site starts returning errorsIts pages not refreshedError rate per hostPer-host backoff and breaker; retry later
Object storage slowFetchers back upWrite latencyBuffer and retry; tasks are acked only after storage

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch:

At 10×:

A 45-minute interview

0–4Frame the problem (scale, politeness, recrawl)
4–9Must do, how well, numbers
9–13Nouns, interfaces, data flow
13–20First working version, parked list
20–40Deep dives: frontier (9.1), dedup (9.2), traps (9.4), durability (9.5)
40–45Follow-ups and wrap-up

A 60-minute interview

0–5Frame the problem
5–11Must do, how well, numbers
11–16Nouns, interfaces, data flow
16–24First working version
24–50Deep dives 9.1–9.6 and the final picture
50–60Follow-ups: JavaScript, importance, 10×; wrap-up
LevelWhat a strong answer shows
Mid-levelFrontier queue, fetchers, parser, URL dedup, storage in object storage, robots.txt.
SeniorPriority plus per-host politeness queues, partitioning by host, Bloom filters with stated false-positive costs, content dedup, timeouts and backoff, durable frontier.
Staff+Traps, adaptive recrawl scheduling, DNS and bandwidth as bottlenecks, JavaScript rendering cost, legal and ethical considerations (robots, rate limits, opt-outs).

Common mistakes:

13. Try it, check yourself, read more

Size the URL-seen Bloom filter

10B
1%

Formulas: bits m = −n·ln(p) / (ln 2)², hash functions k = (m / n)·ln 2. An exact set is estimated at ~60 bytes per URL (a hash plus overhead).

How fast can politeness let you go?

100K
1 s

What keeps a crawler from hammering one website?

Each host is fetched only when its delay has passed, while other hosts keep fetchers busy.

What does a Bloom filter false positive mean in a crawler?

"Probably seen" was wrong; the URL is skipped. Bloom filters never give false negatives.

How should the frontier be partitioned across machines?

All of a host's URLs, rate limits and robots rules then live on one node.

At 1 request a second per host, what limits crawling 3,000 pages a second?

Politeness caps each host, so throughput needs at least 3,000 crawlable hosts at every moment.
ResourceWhy
RFC 9309: Robots Exclusion ProtocolThe standard for robots.txt.
Web crawling and indexes (Introduction to Information Retrieval, free online)The frontier design with front and back queues, explained clearly.
Design a web crawler (Hello Interview)Another walkthrough.