Web crawler system design is the process of building a distributed service that starts from seed URLs, fetches pages, extracts links, and repeats, while respecting robots.txt, never overloading any single website, and avoiding duplicate work. A strong interview answer centers on three things: the URL frontier (what to fetch next and when), deduplication (of URLs and of content), and a capacity estimate that justifies how many machines you need. Everything else is supporting detail.
Key Takeaways
- At 1 billion pages per month, a crawler needs about 400 fetches per second on average and roughly 800 at peak, which is a modest number of machines if fetching is asynchronous.
- The URL frontier is the heart of the design: front queues handle priority, back queues give each host its own lane, and a heap of "next allowed fetch time" enforces politeness.
- Use a bloom filter for "seen URL" checks (about 12 GB for 10 billion URLs at 1% false positives) and SimHash fingerprints for near-duplicate content.
- Follow RFC 9309 for robots.txt: cache per host for up to 24 hours, treat 4xx as allow-all and 5xx as disallow-all.
- Traps and failures are where senior candidates separate themselves: per-host budgets, URL normalization, depth limits, and idempotent, checkpointed workers.
How should you open the web crawler interview?
Open by clarifying requirements, because "design a web crawler" can mean a search indexer, a price monitor, or an archive. Spend three to five minutes here. The framework from our guide on how to ace a system design interview applies, but crawlers have a few questions you should always ask.
Questions worth asking:
- What is the purpose? Search indexing, archiving, or data mining changes what you store and how often you recrawl.
- What content types? Assume HTML only unless told otherwise. Images and PDFs multiply storage.
- What scale? A common prompt is 1 billion pages per month.
- Do we recrawl? Freshness matters for news sites and barely matters for static documentation.
- How long do we keep pages? Assume five years for archive-style storage.
State the requirements back. Functional: given seed URLs, fetch pages, extract and enqueue links, store content, recrawl on a schedule. Non-functional: scalable to billions of pages, polite to every host, resilient to malformed pages and hostile servers, and extensible so new content types or processors can be plugged in later.
What is the capacity estimate for 1 billion pages per month?
The estimate turns a vague prompt into concrete hardware. Work it out loud, state each assumption, and round aggressively. The table below uses an assumed average of 100 KB of HTML per page.
| Metric | Calculation | Result |
|---|---|---|
| Average fetch rate | 1,000,000,000 / (30 x 86,400 s) | ~386 pages/s |
| Peak fetch rate | 2x average | ~800 pages/s |
| Raw HTML per month | 1B x 100 KB | ~100 TB |
| Stored per month (compressed ~5x) | 100 TB / 5 | ~20 TB |
| Five-year storage | 20 TB x 60 months | ~1.2 PB (before replication) |
| Average download bandwidth | 386 x 100 KB/s | ~39 MB/s, ~310 Mbps |
| URL dedup set (10B URLs) | bloom filter, 1% FPR, ~9.6 bits/URL | ~12 GB |
| Content fingerprints | 1B x 8 bytes (64-bit SimHash) | ~8 GB/month |
The 100 KB figure is an interview assumption, not a measurement. Real pages vary widely, so say that out loud and offer to rerun the math with the interviewer's number.
Now convert the rate to machines. A fetch takes somewhere between a few hundred milliseconds and a few seconds when you include DNS, TCP and TLS setup, and slow servers. If the average is about one second, you need roughly 800 requests in flight at peak. One machine running asynchronous I/O can comfortably hold hundreds to low thousands of open connections, so the fetch layer is a handful of machines plus headroom. The real bottlenecks are politeness (you cannot fetch faster from any single host) and parsing CPU, not network.
What does the high-level architecture look like?
The high-level design is a loop. URLs flow from the frontier to fetchers, pages flow to parsers, and new links flow back into the frontier after filtering. Draw it in order:
- Seed URLs enter the URL frontier.
- URL frontier picks the next URL respecting priority and politeness.
- DNS resolver (with a local cache) turns hostnames into IPs.
- Fetchers download pages, honoring robots.txt and timeouts.
- Content store saves the raw page (object storage) and metadata (a wide-column or key-value store).
- Content dedup checks a fingerprint and drops duplicates before parsing.
- Parser / link extractor pulls out links and normalizes them.
- URL filter applies robots rules, blocklists, and trap heuristics.
- URL dedup (bloom filter plus a durable URL store) drops already-seen URLs.
- Surviving URLs go back into the URL frontier.
Connect stages with durable queues (Kafka or a similar log) so each stage scales independently and a crash in the parser does not lose fetched pages. This is the same decoupling pattern you would use in a notification system design, and naming the parallel shows the interviewer you recognize reusable patterns.
How does the URL frontier handle priority and politeness?
The URL frontier is a two-stage queue system: front queues decide what is important, back queues decide what is safe to fetch right now. This split comes from the Mercator crawler design and is described in detail in Manning, Raghavan, and Schütze's Introduction to Information Retrieval. Going this deep is the single best way to stand out on this question.
Front queues: priority
A prioritizer assigns each incoming URL a score and places it in one of F front queues, say F = 10, with queue 1 being the highest priority. Inputs to the score include:
- PageRank-style link importance or the host's historical quality
- How often the page has changed in past crawls (frequently changing pages get recrawled sooner)
- Depth from the seed (shallower usually matters more)
A biased selector pulls from front queues, preferring high-priority queues but still draining low-priority ones so nothing starves.
Back queues: politeness
Politeness means a crawler never sends requests to one host faster than that host can handle. The design that makes this simple is the invariant one host per back queue:
- There are B back queues. A common heuristic from the same textbook is about three times as many back queues as fetcher threads, so workers always have a host ready.
- A host-to-queue mapping table records which back queue owns each host.
- When a back queue empties, the router refills it by pulling URLs from the front queues until it finds a host that is not already assigned elsewhere.
- A min-heap holds one entry per back queue, keyed by the earliest time that host may be contacted again.
A fetcher pops the heap, waits until that timestamp if needed, fetches one URL from that back queue, and pushes the queue back onto the heap with a new time. The delay is often set adaptively, for example ten times the duration of the last fetch from that host, so slow servers automatically get gentler treatment.
import heapq
import time
def next_url(heap, back_queues):
ready_at, queue_id = heapq.heappop(heap)
wait = ready_at - time.monotonic()
if wait > 0:
time.sleep(wait)
url = back_queues[queue_id].popleft()
return url, queue_id
def release(heap, queue_id, fetch_seconds, politeness_factor=10):
delay = max(1.0, fetch_seconds * politeness_factor)
heapq.heappush(heap, (time.monotonic() + delay, queue_id))
Production code runs this per frontier shard with async waits, but the logic is identical. The heap is effectively a per-host rate limiter, so you can borrow ideas from designing a rate limiter.
Distributing the frontier
At billions of URLs the frontier does not fit on one machine. Shard it by hash of the hostname, not the full URL. Host-based sharding keeps every URL for one site on one node, which means politeness is enforced locally with no cross-node coordination. The trade-off is hot spots for giant sites; handle those by giving very large hosts a dedicated shard and a higher, negotiated crawl rate.
Each shard keeps the tail of each queue in memory and spills the rest to disk, because billions of URLs at around 100 bytes each are hundreds of gigabytes.
The frontier deep dive is exactly where candidates freeze: the interviewer asks "how do you stop two workers from hitting the same host?" and the clock keeps running. TechScreen listens to the question and surfaces a structured answer on your screen in real time, invisible during screen share on Zoom, Google Meet, and Teams. Try it on a mock round with 3 free tokens.
How does a crawler respect robots.txt?
A crawler respects robots.txt by fetching the file before crawling any page on a host, caching the parsed rules, and checking every URL against them before it enters a back queue. The rules are now standardized in RFC 9309, published by the IETF in 2022.
Details worth stating in the interview:
- Caching: RFC 9309 says crawlers should not use a cached robots.txt for more than 24 hours unless the file becomes unreachable. Store it in a per-host cache next to the frontier shard.
- Size: parsers must handle at least the first 500 KiB of the file.
- Status codes: a 4xx response generally means no restrictions. A 5xx or network failure means treat the site as fully disallowed. According to Google's robots.txt documentation, Googlebot stops crawling a site for the first 12 hours after a server error on robots.txt, then falls back to the last good copy for up to 30 days.
- Crawl-delay: this directive is not part of RFC 9309 and Google ignores it, but honoring it when present is a cheap politeness win for your own crawler.
Also send a clear User-Agent with a contact URL so site owners can reach you.
How do fetching, parsing, and storage work?
Fetching, parsing, and storage are the throughput layer, and each has one or two decisions that matter.
DNS. DNS lookups can take tens to hundreds of milliseconds and many resolvers are synchronous. Run a local caching resolver per fetcher cluster and prefetch DNS for hosts as they enter back queues. It is a classic, easily missed bottleneck.
Fetching. Use asynchronous HTTP clients with connection reuse per host, strict timeouts (connect, first byte, and total), a maximum response size (a few MB for HTML), and redirect limits. Send conditional requests with If-Modified-Since or ETag on recrawls so unchanged pages cost a 304 instead of a full download.
Parsing. Parsers are CPU-bound, so run them as a separate pool behind a queue. Extract links, resolve relative URLs against the base, and normalize them: lowercase the scheme and host, remove default ports, drop fragments, sort or strip tracking query parameters. If JavaScript rendering is in scope, route only pages that need it to a much smaller pool of headless browsers, because rendering costs orders of magnitude more than plain parsing.
Storage. Split it by access pattern:
| Data | Store | Why |
|---|---|---|
| Raw page bodies | Object storage (S3, GCS), compressed, batched into large files | Cheap, durable, sequential writes |
| Page metadata (URL, fetch time, status, hash, links) | Wide-column store like Bigtable or Cassandra, keyed by reversed hostname | Fast lookups, range scans per site |
| URL seen set | Bloom filter in memory, backed by a durable key-value store | Constant-time membership check |
| Frontier queues | Memory plus disk-backed log per shard | Size exceeds RAM |
Batching pages into large files, as the WARC archive format does, keeps object storage request costs low. Reversed-hostname keys (com.example.www) group a site's pages for per-site scans.
How do you deduplicate URLs and content?
Deduplication happens at two levels, and you should name both without being asked.
URL dedup. After normalization, hash each URL and check a bloom filter. A bloom filter is a bit array with k hash functions that answers "definitely not seen" or "probably seen." The sizing formula is about -ln(p) / (ln 2)^2 bits per item, which is roughly 9.6 bits at a 1% false positive rate, with about 7 hash functions. For 10 billion URLs that is about 12 GB, small enough to shard across a few frontier nodes. A false positive means skipping a URL you never fetched, which is acceptable at a 1% rate for a crawler. If the interviewer wants zero misses, back the filter with an exact store and only consult it on "probably seen" hits.
Content dedup. Many different URLs serve identical content (mirrors, ?ref= variants, printer-friendly pages). Hash the normalized body with a fast hash to catch exact duplicates. For near-duplicates, compute a SimHash fingerprint. Google researchers Manku, Jain, and Das Sarma showed in a 2007 WWW paper that 64-bit SimHash fingerprints with a Hamming distance threshold of 3 detect near-duplicates effectively across a corpus of billions of pages. Searching for fingerprints within 3 bits uses a set of permuted, sorted tables so the lookup stays fast.
Dedup content before parsing links. Otherwise every mirror page dumps the same hundreds of links back into the pipeline, and the URL bloom filter has to absorb them all.
How do you handle crawler traps and failures?
Traps and failures are where interviewers probe for senior-level judgment. A crawler trap is any URL space that generates unlimited unique URLs, whether malicious or accidental.
Common traps and defenses:
| Trap | Example | Defense |
|---|---|---|
| Infinite calendars | /calendar?month=2026-11, then 2026-12, forever | Per-host page budget, low priority for deep pagination |
| Session IDs in URLs | ?sid=8f3a... on every link | Normalize away known session parameters |
| Path loops | /a/b/a/b/a/b/... from relative links | Max path depth, detect repeating segments |
| Very long URLs | Generated query strings over 2,000 chars | Max URL length filter |
| Spider traps / tarpits | Server responds slowly forever | Total fetch timeout, max response size |
| Soft 404s | 200 status with "not found" content | Content dedup catches the repeated template |
The strongest single defense is a per-host budget tied to the host's priority. A trap can then waste one host's allowance but cannot take over the crawl.
For failures, make every stage idempotent and checkpointed:
- Fetcher crashes: URLs are acknowledged in the frontier only after the page is durably written, so a crash leads to a retry, not a loss. Dedup on write makes the retry harmless.
- Retries: use exponential backoff per host, and after repeated failures demote the host's priority instead of retrying forever.
- Frontier node loss: replicate the disk-backed queue log, and rebuild in-memory heaps from it on restart.
- Bad content: malformed HTML, wrong encodings, and huge files should be caught by parser timeouts and size limits so one page cannot stall a worker.
Structured narration matters most in this section. Our guide on how to think out loud in technical interviews has phrasing that works well for walking through failure scenarios.
What follow-up questions should you expect?
Expect two or three follow-ups once the core design is on the board. Short, confident answers here move a mid-level answer to a senior one.
- How do you decide recrawl frequency? Track change rate per page from past fetches and schedule recrawls in proportion to it. Use conditional GETs so unchanged pages are cheap.
- How do you crawl JavaScript-heavy sites? Classify pages by whether the raw HTML contains meaningful content, then send only the rest to a headless browser pool with its own lower throughput budget.
- How do you scale to 10x? The frontier shards by host and the fetch layer is stateless, so add nodes. The bloom filter grows linearly, around 120 GB for 100 billion URLs at 1%, still shardable.
- How would you prioritize fresh news? Add a fast lane: seeds from RSS feeds and sitemaps go straight to a high-priority front queue with short recrawl intervals.
- What would you monitor? Fetch rate per shard, error rate by status code, queue depth, robots.txt failures, and duplicate ratio. A rising duplicate ratio is often the first sign of a trap.
Related designs reuse the same building blocks. The key-value and caching ideas overlap with designing a distributed cache, the hashing and ID mapping overlap with the URL shortener design, and the ranking side connects to typeahead autocomplete design.
How do you practice this question so it sticks?
Time yourself through the full answer in 45 minutes, then repeat only the frontier deep dive until you can explain front queues, back queues, and the heap in under five minutes without notes.
Then run it as a mock with a peer and ask for follow-ups you have not rehearsed. Our guide on how to practice system design interviews lays out a weekly plan, and the Google technical interview process guide covers how search-heavy loops grade design rounds.
Strong candidates do not just memorize the diagram. They explain why the frontier shards by host and why a bloom filter is acceptable. Those reasons are what interviewers write down.
Want a second brain in your next system design round? TechScreen runs as an invisible desktop overlay, hears follow-ups like "how do you handle crawler traps?" and gives you the capacity math and trade-offs in real time while staying hidden from screen share. Start with 3 free tokens, no credit card needed.
Frequently Asked Questions
What is the most important component in a web crawler system design?
The URL frontier. It decides which URL gets fetched next, so it controls both priority (important pages first) and politeness (never hammering a single host). A good answer splits it into front queues that rank URLs by priority and back queues that each hold URLs for one host, with a min-heap keyed by the next time each host may be contacted. Most of the interesting trade-offs in a crawler interview live in this component.
How do you estimate capacity for a crawler that fetches 1 billion pages per month?
Divide 1 billion by roughly 2.6 million seconds in a month to get about 400 fetches per second on average, then double it for peak, so plan for around 800 per second. At an assumed 100 KB per page that is about 100 TB of raw HTML per month and roughly 300 Mbps of average download bandwidth. Compression cuts stored size several times. State your page-size assumption out loud because it drives every downstream number.
Why use a bloom filter for URL deduplication in a crawler?
A bloom filter answers 'have I seen this URL?' in constant time using very little memory. Ten billion URLs at a 1% false positive rate need about 12 GB, which fits in memory across a few machines, while an exact hash set of the same URLs would need hundreds of gigabytes. The cost is that a false positive makes the crawler skip a URL it never fetched, which is acceptable for a crawler because missing a tiny fraction of pages is tolerable.
How does a web crawler detect near-duplicate pages?
Exact duplicates are caught by hashing the normalized page content and checking the hash against a store. Near-duplicates, such as the same article with a different ad block or timestamp, need a locality-sensitive fingerprint like SimHash. Pages with fingerprints that differ in only a few bits are treated as duplicates. Google published research in 2007 using 64-bit SimHash fingerprints with a Hamming distance threshold of 3 to detect near-duplicates across billions of pages.
How should a crawler handle robots.txt?
Fetch robots.txt before crawling any URL on a host, cache it per host, and check every URL against it before it enters the fetch queue. RFC 9309 says crawlers should not use a cached copy for more than 24 hours unless the file becomes unreachable, and should parse at least the first 500 KiB. A 4xx response generally means no restrictions, while a 5xx response should be treated as a full disallow until the file can be fetched again.
What are crawler traps and how do you avoid them?
Crawler traps are URL spaces that generate endless unique links, like infinite calendar pages, session IDs in query strings, or symbolic link loops that produce ever-deeper paths. Defenses include URL normalization that strips session parameters, a maximum URL length and path depth, a per-host page budget, and detection of repeating path segments. Combining a per-host budget with priority scoring means a trap can waste some of one host's budget but can never starve the rest of the crawl.
Is the web crawler question asked at FAANG system design interviews?
Yes, it is a classic system design prompt and appears in most popular system design prep books, especially for search and infrastructure-heavy roles. It is popular because it tests distributed queues, rate limiting, deduplication, storage, and failure handling in one problem without needing domain knowledge. Senior candidates are expected to go deep on the frontier and politeness rather than only drawing boxes.
Ready to use AI assistance in your next interview?
TechScreen is the invisible AI assistant trusted by engineers interviewing at Google, Meta, Amazon, and hundreds of other companies. Start with 3 free tokens — no credit card required.
Ace your next interview →