Skip to lesson
‹

Courses / System Design Interview / Chapter 32

Module 3 · Design Real Products · Chapter 32 of 39

Design a Web Crawler

20 sections · ~41 min✓ Fact-checked

Builds on Back-of-the-Envelope Estimates, DNS: How a Name Becomes an Address, Designing a Key-Value Store, Message Queues, Rate Limiters, Blob Stores and Distributed Search. Next up: Design Typeahead Suggestions.

Part 1 · Requirements and Scale

The brief

It is the last over of a World Cup final. A cricket news site, news.example.in, is serving millions of readers on servers already near their limit, and it posts a new match report every few minutes. Readers searching for the result want that report in search soon. Now picture a careless crawler that picks this moment to discover the site's 40,000 archive pages and fetches 300 of them a second. It is the push that takes the site down, and the owners block it for good. A good crawler finds the new report quickly and still visits the host gently. The part that strikes that balance, the frontier, is the heart of this chapter.

The product: the crawler behind a search engine. It starts from a list of seed URLs and follows links across the web. It fetches about 100 million pages a day, keeps important pages fresh and hands every page to the indexer, and no website owner should ever feel it as an attack. We follow news.example.in through the whole design.

What a crawler does

A web crawler, also called a spider or a bot, downloads web pages, finds the links inside them and downloads those pages too. Its seed URLs, such as the home pages of news sites, government portals and universities, are only the start: it keeps following links until it has fetched billions of pages. The whole job fits in a loop:

python
frontier = Frontier(seed_urls)
while True:
    url = frontier.next_ready()        # priority and politeness
    page = fetch(url)                  # DNS, robots.txt, HTTP
    store(page)                        # kept for the indexer
    for link in extract_links(page):
        link = normalise(link)
        if not seen(link):             # de-duplication
            frontier.add(link)

Every hard part of this chapter hides inside one of these lines.

Run that loop a thousand times a second for months, on millions of other people's websites, and every line becomes a design problem. The frontier, the crawler's to-do list, sits at both ends: every URL leaves from it and every new link returns to it. next_ready must pick important URLs first without flooding any one site, fetch meets slow servers and pages that never end, and seen must answer for billions of URLs, tens of thousands of times a second.

Here is the scope for today:

In scope todayOut of scope
Fetch HTML pages over HTTP and HTTPSImages, video and other media files
Discover pages by following links and reading sitemapsPages behind logins, forms or paywalls
Obey robots.txt and never overload a siteBuilding the index and ranking results (the distributed search chapter)
Store pages for an indexer to readRunning JavaScript to render pages (an extension for later)
Re-fetch pages so stored copies stay freshJudging spam and page quality beyond simple limits

Everything out of scope plugs in later as an extra stage after fetch; nothing in it changes the loop.

Decide what the crawl is for

A search engine wants breadth and freshness, a price tracker wants a few thousand shops checked every hour, and an archive wants every version of every page. The purpose decides priorities and what gets stored. Here we build the crawler behind a general search engine.

How well it must work

The loop says what the crawler does; this page says how well. A crawler has nobody waiting at a screen, so steady throughput and good manners matter far more than latency:

  • Polite. One request at a time per host, at least a second apart, and slower when the site is slow or asks; robots.txt is always obeyed. To a small website, a careless crawler looks like an attack.
  • High throughput. 100 million pages a day now and ten times that later, by adding machines rather than redesigning.
  • Fresh where it matters. The busiest news home pages and feeds are re-checked every few minutes; an average page about once a month.
  • Resilient to a messy web. Broken HTML, endless link loops, huge files and servers that report errors as success must not stall or flood the crawl.
  • Survives crashes. A dead machine's queues are restored from a checkpoint; at worst some pages are fetched twice.

To size it, assume a web like this:

AssumptionValue
Pages kept in the index3 B
How often each is refreshedAbout once a month on average
URLs known, fetched or not10 B
HTML per pageAbout 100 KB before compression
Links per pageAbout 50
HostsAbout 50 M, very uneven in size

Refreshing 3 B pages once a month means about 100 M fetches a day, and every estimate on the next page starts from that number. New pages compete with refreshes for the same budget, so deciding what not to fetch matters as much as fetching fast.

Estimates: rate, bandwidth and storage

Use the method from the estimates chapter. 100 M fetches a day is 100 M ÷ 86,400 s, about 1,160 pages a second. Crawl traffic has no evening peak: the crawler sets its own pace and keeps it all day, so the average is also the rate to plan for, plus headroom for retries.

Try it first: 100 M pages a day, about 100 KB of HTML each, compressed 4×, kept for a year, stored with 6 + 3 erasure coding from the blob store chapter. How much disk is that?
  • 100 M × 100 KB = 10 TB of HTML a day; ÷ 4 = 2.5 TB stored a day.
  • × 365 days ≈ 910 TB for one copy of a year.
  • × 1.5 for erasure coding ≈ 1.4 PB, an upper bound, because a page that has not changed is not stored again.

About 1.4 PB a year fits on fewer than a hundred 16 TB disks, before spares, so storage is not the hard part. The other numbers show where the pressure is:

QuantityWorkingResult
Fetch rate100 M ÷ 86,400 s≈ 1,160 pages/s
HTTP requestsPlus robots.txt, redirects and retries: about +25%≈ 1,500 /s
Fetches in flight1,500 /s × about 1 s each≈ 1,500 open connections
Download1,160 × 100 KB≈ 116 MB/s, about 1 Gbps
Link checks1,160 pages × 50 links≈ 58,000 seen checks /s
URL seen set10 B URLs × 8-byte hash≈ 80 GB

The third row uses Little's law: requests in flight = rate × time each takes. Most of that second is spent waiting on a distant server, so 1,500 open connections need little CPU. The row that shapes the design is the fifth: 58,000 lookups a second against a set of 10 billion URLs. Ten crawler nodes are a comfortable start: each owns a tenth of the hosts, keeps about 150 connections open and holds 8 GB of the seen set in memory. One number is still missing, how many hosts must be in rotation at once; the frontier deep dive works it out.

Part 2 · API, Data and the Crawl Loop

The API

With the numbers set, the next question is how the rest of the company talks to the crawler. It has no end users, so its API is small and internal: operators add seeds and manage hosts, other systems ask for a page to be fetched again, and the indexer hears about new pages through events instead of calling in.

http
POST /v1/seeds      {"urls": ["https://news.example.in/"], "priority": 0}          -> 202 {"accepted": 1}
POST /v1/recrawl    {"url": "https://shop.example.in/p/881", "reason": "sitemap"}  -> 202
GET  /v1/pages?url=https://news.example.in/scores   -> 200 {"status": 200, "fetched_at": "...", "content_key": "..."}
GET  /v1/hosts/news.example.in                     -> 200 {"gap_ms": 1000, "queued": 1240, "errors_1h": 2}
PUT  /v1/hosts/news.example.in/policy   {"max_per_sec": 0.2, "blocked": false}     -> 204

202 means queued, not fetched: the frontier decides when.

202 Accepted is the honest answer. A seed or a recrawl request joins the frontier, and the fetch may happen in seconds or in days, depending on its priority and the host's politeness gap. Nothing outside the crawler should wait on a fetch.

The host policy call is the emergency brake. If news.example.in's owners complain during the final, an operator slows the crawler for that host or blocks it, and every fetcher obeys within seconds, because the frontier reads the policy before handing out the host's next URL. The same call can raise the limit for large sites that are happy to be crawled faster.

Data model

Behind those calls sit a few tables. Everything is keyed by a URL hash, a 64-bit hash of the normalised URL: URLs average about 100 bytes and the hash is 8, so sets and tables keyed by it stay small. Hosts get their own table, because politeness, robots.txt rules and DNS answers belong to a host, not to a page.

hostshost (pk)ip_addressdns_expires_atrobots_rulesrobots_fetched_atgap_msnext_allowed_aterror_count
urlsurl_hash (pk)urlhostprioritydepthnext_fetch_atrefresh_intervaletaglast_modified
fetchesurl_hash (pk part 1)fetched_at (pk part 2)http_statuscontent_keycontent_hashsimhash
contentcontent_hash (pk)first_url_hashcontent_key
linksfrom_url_hashto_url_hashanchor_text

Each kind of data goes where its access pattern is cheap:

DataStoreWhy
Frontier queuesAn embedded on-disk key-value store on each node, keys sorted by hostBillions of entries; one host's URLs sit together, in order
URL and host recordsWide-column store, partitioned by hostUpdated after every fetch; read by URL hash or by host
URL seen setMemory on each node: 8-byte hashesChecked about 58,000 times a second
PagesBlob store, about 2,600 compressed pages packed into each 64 MB objectWritten once, read in bulk by the indexer
LinksAppend-only files, read by a daily batch jobUsed to score page importance offline

The frontier and the seen set live on the crawler nodes themselves, because they are touched on every fetch; everything else can sit in shared stores.

Pages are packed because a 25 KB compressed page is a tiny object, and the blob store chapter warned that per-object overhead then outweighs the data; a content_key looks like pages/n07/f31@88123. The links table feeds an offline job that scores importance: a page linked from many different sites is probably worth fetching first, so that score becomes one input to the frontier's priority.

The crawl loop

Now put the pieces in motion. Split the web across crawler nodes by host: each host name is hashed onto a ring with virtual nodes, as in the key-value store chapter, and the node that owns it runs the whole loop for it. All the state for one host lives on one machine: its queue, its politeness timer, its robots.txt rules and its DNS answer, so two machines can never hit the same site at once by accident.

The scene follows one URL from news.example.in around a node's loop. The danger it shows is waste: most links on a page are ones the crawler already knows, and some pages are copies of others.

Animated sceneOne URL through the crawl loop0:23

First visit to the host: look up its address once and fetch its robots.txt rules

Read it as steps

A page from news.example.in is fetched and parsed; 48 links go in, only 7 new URLs reach the frontier, and a duplicate report is caught.

The seen check is the frontier's gatekeeper: without it, every page would re-add dozens of known URLs and the frontier would grow without end. The fingerprint check does the same job for content, so the page store holds one copy of the report.

Every node runs this loop on its own slice of the web, sending links for hosts it does not own to their owners in batches every second or so. A URL handed to a fetcher is marked in flight with a deadline, like the visibility timeout in the message queue chapter; if the fetcher dies, the URL is handed out again, and fetching a page twice is harmless. The scene glossed over one question: which URL should the frontier hand out next, and when may a host be visited again?

Part 3 · Deep Dives

The frontier: what to fetch, and when

Core deep dive

The frontier is where the crawler decides both what matters and how to stay welcome; every other part of the design feeds it or drains it.

Think of a teacher taking questions from a crowded class: the most urgent go first, but no single student may ask ten in a row. The frontier is not a first-in, first-out queue. A large share of the links on a news.example.in page point back into the same site: menus, related reports, footers. Queue them in a row and the fetchers either hit that one site in a burst or sit idle waiting for it while thousands of other sites are ready. So the frontier answers two separate questions: what to fetch first, and when each host may be visited again.

How a URL moves through the frontier
  1. 1Score the URLpriority 0 (highest) to 3, from importance, change rate and where the link was found
  2. 2Front queues, one per prioritythey decide what comes first and know nothing about hosts
  3. 3A selector drains them unevenlypriority 0 about half the time, then 30%, 15% and 5%, so low priorities still move
  4. 4Back queues, one per hosteach URL moves to its host's queue; this layer decides when
  5. 5A heap of hosts by next allowed timethe host at the top may be fetched soonest
  6. 6Fetch one URL, then reschedule the hostback into the heap at reply time plus its gap

Each host's gap follows one rule: the largest of 1 second, the site's Crawl-delay if it sets one, and 3 times its last response time. A server that slows down is probably busy, so the crawler backs off without being told. It is the rate limiter chapter's idea turned around: we limit ourselves, per host, before the site has to.

That rule fixes the missing number from the estimates. A typical 1 s fetch earns a 3 s gap, so each host yields about one page every 4 seconds, and 1,160 pages a second needs over 4,600 hosts in rotation at once; keep about 10,000 active. Speed comes from breadth, never from hitting one host harder. The cost is depth: even at one page a second, a site with 10 M pages takes about four months, so its home and section pages go before its archives.

The frontier under pressure

The rule is simple on a quiet day. The final is not quiet: news.example.in slows down, then starts refusing requests, while 1,240 of its URLs wait in its back queue and two fetchers want work.

Predict before you watch: news.example.in just answered slowly and must now wait 4 seconds. What do the fetchers do during those 4 seconds?
  • They fetch from other hosts whose turn has come, and the news site gets no request until its gap ends.
Animated sceneOne host waits, the others proceed0:19

Watch for news.example.in: it waits out its gap while the fetchers serve others

Read it as steps

news.example.in slows during the final and then returns a 503; its gap grows each time while the fetchers keep serving other hosts.

The heap turned one host's trouble into a longer wait for that host alone: the fetchers never idled, and the news site saw at most one request at a time, spaced further apart as it struggled. A 503 or 429 doubles the gap and honours any Retry-After the site sends; after repeated errors the host is paused and an operator alerted.

Where the frontier lives

Billions of queued URLs fill hundreds of gigabytes, too much for memory, and one file per host would mean millions of tiny files. So each node keeps its back queues in an embedded key-value store, a library such as RocksDB or LevelDB that runs inside the crawler and writes to local disk. Keys are the host followed by a sequence number, so a host's next URL is the first key under its name. Every few minutes the node checkpoints the store and its host table to the blob store.

Two more limits guard the same promise. Thousands of small sites can share one server, so the frontier also limits requests per IP address. And a host that moves to another node after a crash is treated as just fetched, so a hand-over never gives a site two quick visits.

Duplicate URLs

The frontier only works if it holds each page once. Duplicates come in two kinds. URL duplicates are one page reached through different spellings of its address; content duplicates are the same text at different URLs. This page handles the first kind, caught before a URL enters the frontier.

text
found:      HTTPS://News.Example.in:443/sports/../scores/?utm_source=app&b=2&a=1#live
normalised: https://news.example.in/scores/?a=1&b=2

Lowercase the scheme and host, drop the default port, resolve . and .., drop the #fragment and tracking parameters, then sort what is left.

The parser first resolves each href against the page's own address and drops mailto: and javascript: links. Normalising each link then turns many spellings into one. Leave the path's letter case and trailing slash alone: many servers treat /Scores and /scores as different pages. A <link rel="canonical"> tag, when present, names the address the site prefers.

Then comes the seen set: every URL the crawler has ever queued, checked 58,000 times a second. Four ways to hold 10 B URLs:

Seen set for 10 B URLsMemoryWhat can go wrong
Full URLs in a hash set≈ 1 TBNothing, except the memory bill
8-byte URL hashes≈ 80 GBAbout 3 collisions (n² ÷ 2⁶⁵), so about 3 pages are never fetched
Bloom filter, 1% false positives≈ 12 GB, about 9.6 bits per URL1 in 100 new URLs wrongly judged as seen
Bloom filter, 0.1% false positives≈ 18 GB, about 14.4 bits per URL1 in 1,000 new URLs wrongly judged as seen

Every option trades memory for pages silently skipped.

A Bloom filter, met in the key-value store chapter, is a bit array plus several hash functions; if a lookup finds any of a URL's bits unset, the URL is definitely new. A new URL whose bits all happen to be set is skipped until the filter is rebuilt. At our scale 8-byte hashes fit in memory across ten nodes, so we use them; at ten times the scale the filter starts to pay.

Duplicate pages

Normalising cannot tell that news.example.in/scores and news.example.in/print/scores hold the same report, or that a wire story appears on fifty sites. Those are content duplicates, caught after the fetch, by the parser. They waste disk and, worse, their links flood the frontier with more copies.

For exact copies, hash the page's extracted text, not its raw HTML, so a changed advert or timestamp in the markup does not matter. Equal hashes mean an exact copy: store a pointer to the first copy in the content table and skip its links. That is what happened to the print page in the crawl loop scene.

Near-copies defeat an ordinary hash. Change one character, such as an "updated 10:47" line under the match report, and the whole hash changes. A similarity fingerprint such as SimHash handles them. The intuition: let every short run of words vote, so a few changed words cannot outvote the rest. The mechanism: each run of words is hashed to 64 bits and votes +1 or −1 on each bit position; the fingerprint keeps the sign of each total. Similar pages get fingerprints that differ in only a few bits, and this design treats pages within 3 bits as duplicates.

Finding those among billions of fingerprints needs one more trick. Split each fingerprint into four 16-bit blocks. Two fingerprints within 3 bits must match exactly on at least one block, because 3 differing bits cannot touch all four. So keep four lookup tables, one per block, fetch only the fingerprints that share a block, and compare those bit by bit.

The threshold is the trade-off. At 3 bits, two different reports of the same match, sharing a scorecard and little else, stay separate; raise it to 10 and the crawler starts throwing away genuinely new pages. Copies are now caught. The next danger is pages that are not copies, yet never stop coming.

Traps and limits

Some sites generate URLs without end, by accident or on purpose. A crawler trap is any part of a site that keeps producing new URLs while adding little new content. Every one of those URLs passes the seen check, because it really is new, so without limits the frontier fills with one site's junk:

TrapExampleDefence
Endless calendar/fixtures?month=2026-11 links to the next month, foreverCap pages per host; spot URLs that differ by one number yet return near-identical pages
Endless subdomainsa1.example.in, a2.example.in and so on, each a new hostCap pages per registered domain too
Session ids in URLs/shop;jsessionid=8F2A... is a new URL on every visitStrip known session parameters while normalising
Repeating paths/a/b/a/b/a/b/ from a broken relative linkLimit URL length, path depth and repeated segments
Filter combinations?colour=red&size=9&sort=price turns one listing into thousands of URLsLearn which parameters do not change the content; cap URLs per path pattern
Soft 404sEvery missing page returns 200 with a "not found" messageFetch a made-up URL on the host; if it returns 200, compare fingerprints

The defences share one idea: a site gets a budget, not a blank cheque.

The per-host budget is the frontier's last line of defence, and it is set by importance: news.example.in earns a large budget, a site nobody links to a small one. Traps waste the budget on new URLs. The other claim on it is old URLs that need fetching again.

Freshness: fetching again

A stored page goes stale the moment its site changes it. The frontier must decide when each page is fetched again, without wasting fetches on pages that never change. Each page keeps a refresh_interval. After every fetch, compare the content fingerprint with the last one: if the page changed, halve the interval, down to an hour; if not, double it, up to about 60 days. Important pages get a lower ceiling, so a top news site's pages are re-fetched at least daily. Here is where that rule settles for four kinds of page:

PageHow often it changesWhere the interval settles
News home pageEvery visit1 hour, the floor
Live scorecard during a finalEvery visit until the match ends1 hour, then growing
Product pagePrice changes about weeklyBetween 4 and 8 days
"About us" pageAlmost never1, 2, 4, 8, 16, 32 days, then 60

The rule spends fetches where pages change and starves pages that do not, with no list for anyone to maintain.

An hour is too slow for the final, so the busiest news sites get more: a floor of a few minutes for their home pages, and polling of their feeds and news sitemaps, which list new URLs with a time. That is how the new match report is found within minutes. At one request every 4 s or more, polling one host every few minutes is still gentle.

Two HTTP features make refreshing cheap. Send the ETag or Last-Modified value from the last fetch back as If-None-Match or If-Modified-Since, and an unchanged page comes back as a 304 Not Modified with no body. The deep dives are done; next, what fails when this runs for months.

Part 4 · Failures and Trade-offs

What fails, and how the design copes

A crawler runs for months, so everything on this page will happen sooner or later. The goal each time is the same: lose little work, and never take a problem out on someone else's website.

What goes wrongEffectHow the design copes
A crawler node diesIts hosts stop being crawled; fetches in flight are lostIts hosts move to the next nodes on the ring, which load its last checkpoint; newer links turn up again later
The page store is slow or downParsed pages pile upBuffer them on local disk; if the buffer fills, pause fetching rather than drop pages
DNS lookups slow downEvery fetch waitsLocal caches, asynchronous lookups and more than one upstream resolver
A site slows down or returns 503Our traffic could make it worseThe gap grows with response time and doubles on 503; after repeated errors, pause the host and alert
Thousands of hosts on one server"Different" hosts overload one machineAlso limit per IP address, using a counter all nodes share
A zone or region failsHosts crawled there pause; nothing stored is lostNodes in a dead zone drop off the ring and their hosts move on, as for one node. A dead region's hosts go to the next-nearest region, which loads their checkpoints from a copy kept outside the region

Almost every row ends the same way: the frontier is restored or slowed, and no site pays for our failure.

Growth shifts the balance. At ten times the scale, 1 B pages a day, the fetch rate is about 11,600 pages a second and the frontier needs more than 46,000 hosts in rotation at once. The 8-byte hash set grows to 800 GB for 100 B URLs, and a 1% Bloom filter at about 120 GB starts to look attractive.

Trade-offs, regions and cost

Three tensions run through every choice in this chapter. Each one is a dial on the frontier:

  • Coverage against freshness. Every refresh is a fetch not spent on discovery. Set the split explicitly, such as 70% refreshes and 30% new pages, and watch both.
  • Speed against politeness. Politeness does not grow with your hardware: extra speed must come from more hosts in parallel, never from hitting one harder.
  • Plain HTML against rendering. A headless browser sees content that JavaScript builds, at many times the CPU per page, so only pages that need it go to a separate, smaller pool.

Fetchers in more than one region. A node in Mumbai fetching a site served from the US east coast waits about 200 ms per round trip, several round trips per page, and pulls every byte over long, costly links. So run crawler nodes in three or four regions and give each host to the region nearest its servers, judged from its IP address. A host still belongs to exactly one node, so politeness holds, and links for hosts owned elsewhere travel to their owners in batches, as before.

What dominates the bill: storage. It is not the hard part, but it grows every month: a year of pages, about a petabyte, costs on the order of $20,000 to $30,000 a month at object-storage prices (check current prices), while ten fetch nodes cost far less. Rendering JavaScript would change that, since a headless browser needs many times the CPU per page.

Part 5 · Going further

Fetching: DNS, robots.txt and responses

Optional: the fetch details behind the loop, for when you need to know exactly how robots.txt and HTTP replies are handled.

Every fetch needs the host's IP address. As the DNS chapter showed, a cold lookup often takes 50 to 150 ms, and a shared resolver may not welcome 1,500 queries a second from one client. So each node runs its own caching resolver that keeps answers for their TTL, caches "no such name" too, and looks a host up just before its turn.

Every host has its own robots.txt, a plain-text file at its root that says which paths crawlers may visit; fetch it before the host's first page and keep it for up to a day, as the standard published in 2022 as RFC 9309 recommends.

text
# https://shop.example.in/robots.txt
User-agent: *
Disallow: /cart/
Disallow: /search
Allow: /search/help
Crawl-delay: 2

Sitemap: https://shop.example.in/sitemap.xml

Crawl-delay is not part of the standard, but some crawlers honour it, and ours does.

Three details catch people out. The longest matching rule wins: /search?q=shoes is blocked by Disallow: /search, but /search/help is allowed because Allow: /search/help is the longer match. A group for your crawler's own name replaces the * group instead of adding to it. And a 4xx for robots.txt means no rules apply, while a 5xx or a timeout means treating the whole site as off limits for now. Each HTTP reply then tells the frontier what to do:

ResponseWhat the crawler does
200Parse, store and extract links
301 or 308Moved for good: send the new URL through the seen check and point the old one at it
302, 303 or 307Moved for now: follow, but keep the old URL; at most 5 redirects in a row
304Unchanged since the last fetch: nothing to store; lengthen the refresh interval
404 or 410Gone: retry a 404 once or twice over a few days, drop a 410, tell the indexer
429 or 503Slow down: double the host's gap and honour Retry-After
Other 5xx, timeoutsRetry later with backoff; after repeated failures, lower the host's priority

Errors that blame the site slow the host down; errors that blame the page only reschedule the URL. Finally, send a user-agent such as ExampleBot/1.0 (+https://example.in/bot) linking to a page that explains the crawler, and crawl from published IP addresses, because many owners block crawlers they cannot identify.

Part 6 · Interview kit

The 45-minute answer

A 45-minute interview leaves about 40 minutes of design. Here is the crawler on that clock.

MinutesWhat you say and drawKey number or decision
0–5Purpose: a search engine's crawler. Scope: HTML only, follow links and sitemaps, obey robots.txt, refresh pages, store them for the indexer.Politeness first: one request at a time per host, at least 1 s apart
5–8Estimates that decide something: fetch rate, connections in flight, link checks and the seen set. Storage gets one line.1,160 pages/s; 58,000 seen checks/s; 80 GB of hashes
8–13API: seeds, recrawl, host policy, and a page.fetched event for the indexer. Data: hosts, urls, fetches and content, keyed by a 64-bit URL hash.202 Accepted: the frontier decides when
13–22Draw the loop around the frontier: fetchers with a DNS cache, parser, page store, seen check, back to the frontier. Split hosts across nodes on a hash ring.All of one host's state on one node
22–37The frontier: front queues for priority, back queues per host, a heap of next allowed times, a slow host and a 503. Then de-duplication: normalising, hashes, SimHash.Gap = largest of 1 s, Crawl-delay, 3 × response time; 4,600+ hosts in rotation
37–40Wrap-up: a node crash and its checkpoint, traps and limits, ten times the scale, fetchers in more regions.At 10×: 11,600 pages/s and 46,000+ hosts in rotation

Pick the frontier if they ask how you stay polite or what gets fetched first. Pick de-duplication if they ask how to check billions of URLs or spot the same article on many sites. If they ask about freshness, use the halve-or-double refresh rule.

Say
"Each host yields about one page every 4 seconds, so 1,160 pages a second needs over 4,600 hosts in rotation: speed comes from breadth, never from hitting one site harder."
Say
"Front queues decide what, back queues per host decide when, and each host's gap is the largest of 1 second, its Crawl-delay and 3 times its last response time."
Say
"The seen set is 10 B URLs × 8 bytes = 80 GB across ten nodes; a 1% Bloom filter would cut it to 12 GB but wrongly skip 1 in 100 new URLs."
Avoid
"One big FIFO queue of URLs." It ignores priority and sends bursts to whichever site has many links in a row.

Interview drill

Each question changes one thing about the crawl. Answer it before you open it.

The search team wants 1 B pages a day, ten times today. What changes?
  • About 11,600 pages a second: roughly 100 nodes instead of 10, added to the hash ring a few at a time.
  • Politeness does not scale with hardware, so the frontier must keep more than 46,000 hosts in rotation.
  • The seen set grows to 800 GB of hashes for 100 B URLs; a Bloom filter of about 120 GB starts to pay off.
  • Download nears 10 Gbps, another reason to spread fetchers over several regions.
Product wants an archive: every version of every page, readable as of any date. What changes in the data model?
  • fetches is already keyed by (url_hash, fetched_at), so keep every row instead of only the latest.
  • Store content once per content hash, so an unchanged fetch adds a row, not a copy.
  • "The page as of a date" becomes one range read on (url_hash, fetched_at).
  • Storage becomes the main cost: move old versions to an archive tier 20 to 50 times cheaper.
The budget is cut in half: 50 M fetches a day instead of 100 M. What do you give up, and what do you protect?
  • Protect politeness and the important pages: home pages, news and popular sites keep their refresh rates.
  • Raise the refresh ceiling for unimportant pages, say from 60 to 120 days, and slow discovery on low-value hosts.
  • Lean on conditional requests and sitemaps, so an unchanged page costs a cheap 304.
Common miss
Speeding up by ignoring a host's gap or robots.txt. Extra speed must come from more hosts in parallel.
Common miss
Checking "seen" against a database on disk. 58,000 random lookups a second need the set in memory, split by host.

Wrap-up

Next, Design Typeahead Suggestions swings the other way: a person is waiting, and every answer must arrive within about 100 ms of a keystroke.

Part 7 · Check yourself

Quick check

Q1A crawler keeps one first-in, first-out queue. With politeness on, its fetchers sit idle; with it off, news.example.in complains of bursts. Explain both.

Think it through, then check.

Q2Product now wants a price tracker: 50,000 product pages on one large shop, each checked every hour. What changes?

Think it through, then check.

Q3With a fixed 100 M fetches a day, one team wants new pages found sooner and another wants stored pages fresher. How do you split the budget?

Think it through, then check.

Q4The node that owns news.example.in and 5 M other hosts dies. What happens to them, and what is lost?

Think it through, then check.

Q5Which building block holds each node's per-host queues, and why not a shared database or a message queue?

Think it through, then check.

Next chapter · 33 of 39Design Typeahead Suggestions~40 min · 4 animated scenes