AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content
SD notes

System Design · Note 21 of 22

Design a Web Crawler

Intermediate · 8 min read · updated 14 November 2026

Quick answer

How do you design a web crawler?

Start from seed URLs in a frontier queue. Fetchers take URLs from it, download each page, store it, extract its links, normalise them and add the unseen ones back to the frontier, crawling the link graph breadth first. Per-host queues keep the crawler polite, robots.txt decides what may be fetched, and URL and content hashes stop the same page being crawled twice.

On this page
  1. Requirements
  2. Capacity estimate
  3. API
  4. Data model
  5. High-level design
  6. BFS over the link graph
  7. The frontier and politeness
  8. robots.txt
  9. Dedup: URLs and content
  10. Distributed crawling
  11. Bottlenecks and trade-offs
  12. Interview questions
  13. Common questions

Crawl billions of pages without crawling any twice or overloading any site: a queue, two seen-sets and a lot of politeness.

Requirements

  • Functional: from seed URLs, fetch pages, store them, extract links, keep crawling; recrawl pages that change.
  • Non-functional: scale to billions of pages; polite to every host; robust to bad HTML, slow servers and traps; extensible (new content types).
  • Out of scope unless asked: ranking and the search index itself.

Capacity estimate

Assumptions, not facts — state them, then derive.

AssumptionValueDerivedValue
Pages a month1 billionFetches per second1B ÷ 2.592M s ≈ 386/s
Average page size100 KBStorage a month100 TB
URLs ever seen10 billionURL seen-set (8-byte hashes)80 GB

API

A crawler has no public API; its workers share internal calls like these:

frontier.next(worker)           → url            (a host whose delay has passed)
frontier.add(urls)              → enqueued only the unseen ones
fetch(url)                      → status, headers, body
store.put(urlHash, body, meta)  → saved for the indexer

Data model

StoreKey → valueNotes
Frontierhost → queue of URLs, next-allowed timeMostly on disk, head in memory
URL seen-sethash(normalised URL) → seenHash set or Bloom filter
Content seen-sethash(body) → URLExact duplicates
Page storeURL hash → body, fetched_atBlob store
robots cachehost → rules, fetched_atRefresh about daily

High-level design

  • Frontier hands out URLs; fetchers download them (with their own DNS cache).
  • Parser extracts links; URL filter normalises, applies robots.txt and dedups.
  • Content dedup hashes each body; new pages go to the page store for indexing.
  • The web is a graph: pages are nodes, links are edges.
  • Crawl breadth first from seeds: a FIFO frontier and a seen-set.
  • A page linked many times is still fetched once.
Breadth-first crawl: a FIFO frontier and a seen-set

The crawl starts from a seed URL, A, in the frontier — a FIFO queue of pages to fetch — and in the seen-set, which records every URL ever queued.

0:01 / 0:19Step 1 of 7
Transcript · 7 steps

The frontier and politeness

  • Front queues order URLs by priority (importance, freshness).
  • Back queues, one per host, each with a next-allowed time.
  • At most one fetch per host per delay; the fetcher picks a ready host.
Politeness: per-host queues, one fetch per host per delay

The frontier keeps one queue per host. The fetcher may hit a host at most once every 3 ticks, so a crawler with many workers never floods one small site.

0:01 / 0:25Step 1 of 8
Transcript · 8 steps

robots.txt

  • Fetch each host's robots.txt before crawling it; cache it (about a day).
  • The longest matching rule wins; Allow wins a tie; no match means allowed.
  • It is a request, not access control: a polite crawler obeys it.
robots.txt: the longest matching rule decides

Before crawling a host, fetch its robots.txt once and cache it (for up to a day). Each URL is checked against the rules before it enters the frontier.

0:01 / 0:16Step 1 of 6
Transcript · 6 steps

Dedup: URLs and content

  • Normalise URLs: lower-case host, drop #fragment, drop tracking parameters.
  • Check the URL seen-set before fetching; a Bloom filter saves memory but allows rare false "seen".
  • Hash the content after fetching: mirrors and duplicate pages are dropped.
  • Near-duplicates need a similarity hash (SimHash), not an exact one.
Dedup: normalise and check URLs, then hash and check content

Two checks keep the crawl from wasting work: a URL seen-set before fetching, and a content seen-set after. Both compare a normalised form, not the raw text.

0:01 / 0:17Step 1 of 5
Transcript · 5 steps

Distributed crawling

  • Partition the frontier by host with consistent hashing: one worker owns a host's queue and politeness.
  • Workers forward discovered URLs to the owning worker; the seen-sets are sharded by hash.
  • Checkpoint the frontier so a crashed worker resumes, not restarts.

Bottlenecks and trade-offs

  • DNS lookups are slow: cache them per worker.
  • Freshness vs coverage: recrawl changing pages more often, at the cost of new ones.
  • Bloom filter vs exact set: memory against occasionally skipping a page never fetched.
  • Traps and spam: cap URL length, depth and pages per host.

Interview questions

  • BFS or DFS? BFS: important pages first, load spread across hosts, no endless dives.
  • How do you stay polite? Per-host queues with a delay, one connection per host, robots.txt obeyed.
  • How do you avoid duplicates? Normalised URL seen-set before fetching, content hash after.
  • How do you scale out? Shard the frontier by host so each host's politeness lives on one worker.

Next: Design Search Autocomplete · Before this: Design a Ride-Hailing App (Uber)

Common questions

Why does a crawler use BFS rather than DFS?

Breadth-first search reaches the pages closest to the seeds first, which tend to be the most important ones, and spreads requests across many sites. Depth-first search follows one site's links deeper and deeper, can fall into endless generated pages, and keeps hammering one host.

What is a URL frontier?

The frontier is the queue of URLs still to fetch. In a real crawler it has two parts: front queues that order URLs by priority, and back queues, one per host, that enforce politeness by letting a host be fetched only after its delay has passed.

What is politeness in a web crawler?

Politeness means never overloading a website: at most one connection per host at a time and a delay between requests to it, plus obeying robots.txt. It is enforced by giving each host its own queue and a next-allowed time.

How does a crawler avoid crawling the same page twice?

It normalises every URL — lower-case host, no fragment, no tracking parameters — and checks it against a seen-set before fetching. After fetching, it hashes the page content and checks that against a second set, which catches mirrors and duplicate pages under different URLs.

What is a crawler trap?

A crawler trap is a site that generates endless distinct URLs, such as a calendar with a next-month link forever or session ids in every link. Crawlers defend with limits on URL length, path depth and pages per host, and by spotting repeating path patterns.

How do you distribute a crawler across many machines?

Partition the frontier by host, for example with consistent hashing on the hostname, so each host's URLs and politeness state live on one worker. Workers send discovered URLs for other hosts to the worker that owns them.

Design a Web Crawler: System Design Case Study

Design a web crawler step by step: the URL frontier, BFS over links, per-host politeness, robots.txt, URL and content dedup, and distributed fetchers.

  • Subject: System Design
  • Level: Intermediate
  • Reading time: 8 min
  • Updated: 2026-11-14

How do you design a web crawler?

Start from seed URLs in a frontier queue. Fetchers take URLs from it, download each page, store it, extract its links, normalise them and add the unseen ones back to the frontier, crawling the link graph breadth first. Per-host queues keep the crawler polite, robots.txt decides what may be fetched, and URL and content hashes stop the same page being crawled twice.

Crawl billions of pages without crawling any twice or overloading any site: a queue, two seen-sets and a lot of politeness.

Requirements

  • Functional: from seed URLs, fetch pages, store them, extract links, keep crawling; recrawl pages that change.
  • Non-functional: scale to billions of pages; polite to every host; robust to bad HTML, slow servers and traps; extensible (new content types).
  • Out of scope unless asked: ranking and the search index itself.

Capacity estimate

Assumptions, not facts — state them, then derive.

AssumptionValueDerivedValue
Pages a month1 billionFetches per second1B ÷ 2.592M s ≈ 386/s
Average page size100 KBStorage a month100 TB
URLs ever seen10 billionURL seen-set (8-byte hashes)80 GB

API

A crawler has no public API; its workers share internal calls like these:

frontier.next(worker)           → url            (a host whose delay has passed)
frontier.add(urls)              → enqueued only the unseen ones
fetch(url)                      → status, headers, body
store.put(urlHash, body, meta)  → saved for the indexer

Data model

StoreKey → valueNotes
Frontierhost → queue of URLs, next-allowed timeMostly on disk, head in memory
URL seen-sethash(normalised URL) → seenHash set or Bloom filter
Content seen-sethash(body) → URLExact duplicates
Page storeURL hash → body, fetched_atBlob store
robots cachehost → rules, fetched_atRefresh about daily

High-level design

  • Frontier hands out URLs; fetchers download them (with their own DNS cache).
  • Parser extracts links; URL filter normalises, applies robots.txt and dedups.
  • Content dedup hashes each body; new pages go to the page store for indexing.
  • The web is a graph: pages are nodes, links are edges.
  • Crawl breadth first from seeds: a FIFO frontier and a seen-set.
  • A page linked many times is still fetched once.
ABCDEFfrontier (FIFO)empty — crawl doneseen: A B C D E F
Breadth-first crawl: a FIFO frontier and a seen-set.
  1. The crawl starts from a seed URL, A, in the frontier — a FIFO queue of pages to fetch — and in the seen-set, which records every URL ever queued.
  2. Fetch A and extract its links (B, C). B and C are new, so they join the back of the frontier.
  3. Fetch B and extract its links (A, D). D is new, so it joins the back of the frontier. A is already seen and skipped.
  4. Fetch C and extract its links (D, E). E is new, so it joins the back of the frontier. D is already seen and skipped.
  5. Fetch D and extract its links (F). F is new, so it joins the back of the frontier.
  6. Fetch E and extract its links (A, F). A and F are already seen and skipped.
  7. Fetch F and extract its links (C). C is already seen and skipped. The frontier is empty: 6 pages fetched once each, 5 repeat links ignored.

The frontier and politeness

  • Front queues order URLs by priority (importance, freshness).
  • Back queues, one per host, each with a next-allowed time.
  • At most one fetch per host per delay; the fetcher picks a ready host.
Fetchert = 6a.comfetching nowb.orgdonec.netdonefetched, in ordera/b/c/a/jobsb/faqidlea/blog
Politeness: per-host queues, one fetch per host per delay.
  1. The frontier keeps one queue per host. The fetcher may hit a host at most once every 3 ticks, so a crawler with many workers never floods one small site.
  2. At t=0 the fetcher takes a.com/, the ready host that has waited longest, and marks a.com not ready again until t=3.
  3. At t=1 the fetcher takes b.org/, the ready host that has waited longest, and marks b.org not ready again until t=4.
  4. At t=2 the fetcher takes c.net/, the ready host that has waited longest, and marks c.net not ready again until t=5.
  5. At t=3 the fetcher takes a.com/jobs, the ready host that has waited longest, and marks a.com not ready again until t=6.
  6. At t=4 the fetcher takes b.org/faq, the ready host that has waited longest, and marks b.org not ready again until t=7.
  7. At t=5 every host with URLs left is still inside its delay, so the fetcher waits. With millions of hosts in a real frontier, some host is almost always ready.
  8. At t=6 the fetcher takes a.com/blog, the ready host that has waited longest, and marks a.com not ready again until t=9. Every URL fetched, no host hit twice within 3 ticks.

robots.txt

  • Fetch each host's robots.txt before crawling it; cache it (about a day).
  • The longest matching rule wins; Allow wins a tie; no match means allowed.
  • It is a request, not access control: a polite crawler obeys it.
shop.example/robots.txtUser-agent: *Disallow: /cartDisallow: /searchAllow: /search/helpURLverdict/products/42fetch/cart/checkoutskip/search?q=tvskip/search/helpfetch
robots.txt: the longest matching rule decides.
  1. Before crawling a host, fetch its robots.txt once and cache it (for up to a day). Each URL is checked against the rules before it enters the frontier.
  2. /products/42 matches no rule, so it is allowed: robots.txt lists what to avoid, and everything else is open.
  3. /cart/checkout matches Disallow: /cart, so it is skipped.
  4. /search?q=tv matches Disallow: /search, so it is skipped.
  5. /search/help matches Allow: /search/help, the longest of 2 matching rules, so it is fetched. The most specific rule wins.
  6. 2 of 4 URLs may be fetched. robots.txt is a request, not a lock: a polite crawler obeys it, and many also honour a Crawl-delay line, which is outside the standard.

Dedup: URLs and content

  • Normalise URLs: lower-case host, drop #fragment, drop tracking parameters.
  • Check the URL seen-set before fetching; a Bloom filter saves memory but allows rare false "seen".
  • Hash the content after fetching: mirrors and duplicate pages are dropped.
  • Near-duplicates need a similarity hash (SimHash), not an exact one.
discovered linkhttps://news.example/bnormalised URLURLhashcontenthttps://news.example/anewd44fnewhttps://news.example/aseen——https://m.news.example/anewd44fduplicatehttps://news.example/bnew82benew
Dedup: normalise and check URLs, then hash and check content.
  1. Two checks keep the crawl from wasting work: a URL seen-set before fetching, and a content seen-set after. Both compare a normalised form, not the raw text.
  2. Normalising lower-cases the host, drops the #fragment and the utm_ tracking parameter. The URL is new, so it is fetched; its body hashes to d44f.
  3. https://news.example/a normalises to a URL already in the seen-set, so it is dropped without a fetch.
  4. https://m.news.example/a is a new URL, so it is fetched, but its body hashes to d44f, already seen: a mirror. It is not indexed again and its links are not followed again.
  5. https://news.example/b is new and its body hashes to 82be, new as well, so the page is stored and its links join the frontier. Four links found, two pages kept.

Distributed crawling

  • Partition the frontier by host with consistent hashing: one worker owns a host's queue and politeness.
  • Workers forward discovered URLs to the owning worker; the seen-sets are sharded by hash.
  • Checkpoint the frontier so a crashed worker resumes, not restarts.

Bottlenecks and trade-offs

  • DNS lookups are slow: cache them per worker.
  • Freshness vs coverage: recrawl changing pages more often, at the cost of new ones.
  • Bloom filter vs exact set: memory against occasionally skipping a page never fetched.
  • Traps and spam: cap URL length, depth and pages per host.

Interview questions

  • BFS or DFS? BFS: important pages first, load spread across hosts, no endless dives.
  • How do you stay polite? Per-host queues with a delay, one connection per host, robots.txt obeyed.
  • How do you avoid duplicates? Normalised URL seen-set before fetching, content hash after.
  • How do you scale out? Shard the frontier by host so each host's politeness lives on one worker.

Next: Design Search Autocomplete · Before this: Design a Ride-Hailing App (Uber)

Common questions

Why does a crawler use BFS rather than DFS?

Breadth-first search reaches the pages closest to the seeds first, which tend to be the most important ones, and spreads requests across many sites. Depth-first search follows one site's links deeper and deeper, can fall into endless generated pages, and keeps hammering one host.

What is a URL frontier?

The frontier is the queue of URLs still to fetch. In a real crawler it has two parts: front queues that order URLs by priority, and back queues, one per host, that enforce politeness by letting a host be fetched only after its delay has passed.

What is politeness in a web crawler?

Politeness means never overloading a website: at most one connection per host at a time and a delay between requests to it, plus obeying robots.txt. It is enforced by giving each host its own queue and a next-allowed time.

How does a crawler avoid crawling the same page twice?

It normalises every URL — lower-case host, no fragment, no tracking parameters — and checks it against a seen-set before fetching. After fetching, it hashes the page content and checks that against a second set, which catches mirrors and duplicate pages under different URLs.

What is a crawler trap?

A crawler trap is a site that generates endless distinct URLs, such as a calendar with a next-month link forever or session ids in every link. Crawlers defend with limits on URL length, path depth and pages per host, and by spotting repeating path patterns.

How do you distribute a crawler across many machines?

Partition the frontier by host, for example with consistent hashing on the hostname, so each host's URLs and politeness state live on one worker. Workers send discovered URLs for other hosts to the worker that owns them.

← Design a Ride-Hailing App (Uber) · Design Search Autocomplete (Typeahead) →