System Design Road

Medium Interview walkthrough

Web crawler: the interview, played out

A web crawler sounds like a loop: download a page, find its links, repeat. The interview is about everything that loop gets wrong at a billion pages a month, from hammering a small website to getting stuck forever in a calendar that never ends.

11 min read2 interactive diagrams13 sections

This gives away every stage. For a clean attempt, practise it first, then come back.

The short version

  • The crawl is limited by politeness, not speed. Keep one queue per website and a “not before” time for each.
  • Send each website to one fetcher, so staying polite needs no coordination between machines.
  • Normalise URLs and check a Bloom filter so the same page isn’t fetched twice.
  • Fingerprint page content to spot copies, including near-copies.
  • Re-crawl pages by how often they change, and ask “has it changed?” to save downloads.

The 45 minutes

Before we start

Every search engine starts with a crawler. Google, Bing and every smaller one send programs around the web, downloading pages and following links, so that when you search, they already have a copy of the page.

The basic idea fits on a napkin: keep a list of URLs to visit, take one, download it, add the links you find to the list, repeat. A student can write that in an afternoon. The interview is about what goes wrong at scale. You must not overload anyone’s website. You must not download the same page a thousand times. And you must not get trapped by sites that generate links forever.

Same order as always: requirements, quick maths, data and API, a design that grows, then deep dives.

Minutes 0–5Requirements

Interviewer

Interviewer: Design a web crawler for a search engine. Start from some seed websites, download pages, follow their links, and keep going.

You

You: Is this for a general search engine, or something narrower, like tracking prices?

Interviewer

Interviewer: A general search engine. Another team builds the search index from the pages we save.

You

You: How many pages, and how quickly?

Interviewer

Interviewer: About a billion pages a month.

You

You: Only HTML, or images and PDFs too?

Interviewer

Interviewer: HTML only for now.

You

You: How should we treat the websites themselves?

Interviewer

Interviewer: Politely. Follow their robots.txt rules, and never hit any one site hard enough to slow it down.

You

You: Do pages need to be re-crawled to stay fresh?

Interviewer

Interviewer: Yes. News sites change every few minutes; most pages almost never change.

You

You: What about the same page appearing at several addresses?

Interviewer

Interviewer: That happens a lot. Don’t fetch the same URL twice, and flag pages whose content we already have.

You

You: Do we need to run JavaScript to see pages?

Interviewer

Interviewer: Not for now.

Functional requirements

  1. Start from seed URLs, download pages, extract their links, and keep crawling.
  2. Save each page’s HTML for the indexing team.
  3. Follow each site’s robots.txt and never overload a site.
  4. Re-crawl pages so our copies stay fresh.
  5. Don’t fetch the same URL twice, and flag duplicate content.

Out of scope: building the search index (another team’s job), and running JavaScript (a separate, much more expensive kind of crawler).

Non-functional requirements

  1. Scale: about a billion pages a month.
  2. Polite: at most about one request a second to any one site, unless its rules say otherwise.
  3. Robust: slow servers, broken HTML and endless link traps mustn’t stall the crawl.
  4. Efficient: don’t waste downloads on duplicates or pages that haven’t changed.
  5. Easy to extend to new content types later.

Minutes 5–10Back-of-the-envelope estimates

You

You: A billion pages a month is about 400 pages a second, around the clock.

You: At about 100 KB a page, that’s 40 MB a second of downloads, roughly 320 megabits a second. Very manageable.

You: Storage: a billion pages at 100 KB is 100 TB a month raw, maybe 25 TB compressed. That’s object storage, not a database.

You: We’ll have seen maybe 10 billion URLs. Checking “have we seen this?” against a table that size for every link would be slow. A Bloom filter, which I’ll explain in a moment, does it in about 12 GB of memory.

You: And politeness: if each site gets at most one request a second, then to do 400 pages a second we must always be working on at least 400 different sites at once.

WhatWorkingResult
Pages1 billion ÷ 2.6 million seconds≈ 400 a second
Download400 × 100 KB≈ 40 MB/s (about 320 Mbps)
Storage1 billion × 100 KB, compressed 4×≈ 25 TB a month
Seen URLs10 billion URLs, 1% mistakes allowed≈ 12 GB as a Bloom filter
Politeness400 pages/s, one per site per secondat least 400 sites in progress at once

That last line is the key insight. Raw speed is easy. What limits us is spreading work across enough different sites to stay polite.

Minutes 10–15Core entities and the API

Core entities

  • URL to visit: the address, its priority, and which website it belongs to. The list of these is called the frontier.
  • Page: a URL we’ve fetched, when, the result, a fingerprint of its content, and where the HTML is stored.
  • Website (host): its robots.txt rules, how long to wait between requests, and when we last visited.

The API

A crawler has no public users, so its “API” is how its parts talk to each other:

FUNCTIONfrontier.next(worker_id)

A fetcher asks for its next URL. The frontier only hands out a site’s URL when that site is ready for another request.

Returns
{ "url": "https://example.com/news/42", "not_before": "2026-11-10T10:42:07Z" }
FUNCTIONfrontier.add(urls)

A parser adds the links it found. URLs we’ve already seen are dropped.

Query
urls = [“https://example.com/news/43”, “…”]
Returns
{ "added": 12, "already_seen": 31 }
POST/v1/seeds

The team adds new starting points.

Body
{ "urls": ["https://news.example.com"] }
Responses
  • 202Queued.

Minutes 15–25The design, built up

Stage 1One machine, one queue

URL queueCrawlerfetch, parse, repeatWebsitesVisited set + pagesnext URLGETsavenew linksFine for a few thousand pages.One machine, and it hits sites as fast as it can.
97%
You

You: The simplest crawler: one program with a queue of URLs and a set of URLs it has visited. Take a URL, download it, save the page, pull out the links, add the new ones to the queue, repeat.

This works for a few thousand pages, and it’s exactly what most people write first. But one machine can’t download a billion pages a month. And there’s a worse problem: it fetches as fast as it can, so if a page links to 500 pages on one small site, it fetches all 500 back to back and slows that site down for its real visitors.

Stage 2Many fetchers, a shared frontier, pages in object storage

Seed URLsURL frontiershared list of URLs to visitFetchersmany, by siteNEWWebsitesPage storeobject storageNEWParsersextract linksNEWPage metadatawhen, hash, next crawlNEWnext URLGET pagesave HTMLrecord
59%
Green: new in this stage
You

You: To go faster: many fetcher machines share one frontier, the list of URLs still to visit. Fetchers save pages to object storage, which is cheap and has no size limit, and record each fetch in a metadata store: when, the status, where the page is saved.

You: Parsers run separately. They read saved pages, pull out links and send them back to the frontier. Keeping fetching and parsing apart means a slow parser never holds up downloading.

That’s fast. But we’ve made the politeness problem worse: now many machines can hit the same site at once.

Stage 3Politeness and no repeats: per-site queues, robots, a seen-URL check

Seed URLsURL frontierpriority + one queue per siteFetchersmany, by siteDNS + robots cacheNEWWebsitesPage storeobject storageParsersextract linksSeen URLs + fingerprintsBloom filterNEWPage metadatawhen, hash, next crawlnext URLlook upGET pagesave HTMLnew links?unseen linksrecord
54%
Green: new in this stage
You

You: Two fixes here. First, politeness. The frontier keeps a separate queue for each website, with a “not before” time. A fetcher only gets a URL from a site whose time has passed, and then that site’s time moves forward by a second or so. Fetchers never wait; they just take URLs from other sites.

You: Even better, send each site to one fetcher, by hashing the site’s name. Then one machine owns each site, and keeping it polite needs no coordination between machines.

You: Second, no repeats. Before a link goes into the frontier, we clean it up so different spellings of one address match, then check it against a seen-URL filter. Only new URLs go in. We also cache each site’s address lookups and robots.txt rules so we’re not fetching them over and over.

Here’s the difference per-site queues make:

A page links to 500 pages on one small site
1 min
Frontier500 links to small.siteFetcher 1Fetcher 2Fetcher 3small.siteslow, then blocks usOther sites

The site slows down for its real visitors, and its owner blocks our crawler. We've been rude, and we've lost the site.

Frontier

The hashing trick is the same one from lesson 5.2: the same key always lands on the same machine.

Stage 4Staying fresh: re-crawl by how often pages change

Seed URLsURL frontierpriority + one queue per siteFetchersmany, by siteDNS + robots cacheWebsitesPage storeobject storageParsersextract linksSeen URLs + fingerprintsBloom filterPage metadatawhen, hash, next crawlRe-crawl schedulerby how often pages changeNEWnext URLlook upGET pagesave HTMLnew links?unseen linksrecorddue again
54%
Green: new in this stage
You

You: Last, freshness. Each time we fetch a page, we save a fingerprint of its content. Next time, if the fingerprint changed, the page changes often, so we bring it back sooner. If it hasn’t changed in months, we visit it rarely. A re-crawl scheduler puts pages back into the frontier when they’re due.

Checking the design against the requirements

RequirementWhere the design handles it
Fetch, extract links, keep crawlingFrontier, fetchers and parsers in a loop
Save page HTMLObject storage, with details in the metadata store
Follow robots.txt, never overloadPer-site queues with a “not before” time; cached robots rules
Re-crawl to stay freshScheduler based on how often each page changes
No repeats, flag duplicatesURL clean-up plus a seen-URL filter; content fingerprints
A billion pages a monthMany fetchers, each owning a share of websites
PoliteOne request at a time per site, about a second apart
RobustShort timeouts, per-site queues, page budgets against traps
EfficientNo repeat URLs; “has it changed?” requests save downloads
Easy to extendFetching, parsing and storing are separate steps

Deep diveStaying polite

Interviewer

Interviewer: Say more about politeness. Why is a shared queue a problem?

Bad solutionOne shared queue

URLs are taken in the order they were found. When one page links to 500 pages on the same site, all your fetchers hit that site at once. It slows down, and its owner blocks you.

Good solutionOne queue per site, with a delay

The frontier keeps a queue per website and a “not before” time for each. After handing out one URL from a site, it moves that site’s time forward. Fetchers pick from sites that are ready, so work is spread out naturally.

Great solutionPer-site queues, and each site owned by one fetcher

Hash each site’s name to pick a fetcher, so one machine handles all requests to a given site. Politeness is then a local decision on that machine, with no shared lock or central scheduler. Add a priority layer in front, so important sites are visited first, and you have the classic design from the Mercator crawler, which much of the field still builds on.

Also read each site’s robots.txt, cache it, and obey it. It’s an official standard now (RFC 9309). Some sites also ask for a specific delay between requests; respect that too.

Deep diveNot fetching the same URL twice

Interviewer

Interviewer: How do you avoid fetching a URL you’ve already fetched?

Bad solutionLook it up in a database for every link

A billion pages produce tens of billions of links. A database lookup for each one is far too slow, and the database becomes the bottleneck of the whole crawler.

Good solutionClean up URLs, then check a set in memory

First, normalise each URL so that different spellings match: lowercase the domain, remove #section parts, sort the query parameters, and drop tracking parameters like utm_source. Then check a set of seen URLs held in memory. Fast, but storing 10 billion full URLs takes hundreds of gigabytes.

Great solutionClean up URLs, then check a Bloom filter

A Bloom filter is a compact way to answer “have I seen this?” using a large array of bits. It never says “no” when the answer is “yes”, but now and then it says “yes” when the answer is really “no”. With 1% of those mistakes allowed, 10 billion URLs need only about 12 GB. For a crawler, occasionally skipping a page we’ve never seen is an acceptable price. (Lesson 3.2 shows databases using Bloom filters the same way, to skip files that can’t contain a key.)

Deep diveDuplicate content

Interviewer

Interviewer: Two different URLs return the same page. How do you notice?

You

You: Fingerprint the content. For exact copies, a hash of the HTML is enough: same hash, same page.

You: But most copies aren’t exact. The date in the footer differs, or the ad is different. For those, a similarity fingerprint like SimHash works: pages that are almost the same get fingerprints that differ in only a few bits, so near-copies can be found quickly. Google described using SimHash for exactly this.

You: When we find copies, we tell the indexing team which one to keep, and lower the priority of sites that are mostly copies.

Deep diveCrawler traps

Interviewer

Interviewer: What’s a crawler trap?

You

You: A site that generates endless URLs. A calendar with a “next month” link that never ends. A shop that adds a new session ID to every link. Pages that link to slightly longer versions of themselves. A naive crawler follows them forever.

You: Defences: limit how deep we go into one site, cap URL length, give each site a page budget per crawl, and watch for sites whose pages are mostly near-copies. When a site hits its budget, pause it and move on. Because each site has its own queue, a trap only wastes that site’s share of the crawl, never everyone’s.

Deep diveFreshness without re-downloading everything

Interviewer

Interviewer: How do you keep pages fresh without downloading the whole web again and again?

Bad solutionRe-crawl everything on the same schedule

Visiting every page once a week means news front pages are days out of date, while pages that never change are downloaded over and over for nothing.

Good solutionRe-crawl based on how often a page changes

Compare the content fingerprint with the last visit. If it changed, come back sooner; if not, wait longer. News pages end up checked every few minutes, and old pages every few weeks.

Great solutionChange rate, plus “only send it if it changed”

When re-fetching, send the If-Modified-Since or If-None-Match header from the last visit. If nothing changed, the site replies 304 Not Modified with no page at all, saving bandwidth for both of you. And read sites’ sitemaps, which often list every page with the date it last changed.

Deep diveSlow and broken sites

Interviewer

Interviewer: Some sites are very slow, or broken. How do you stop them stalling everything?

You

You: Short timeouts on every request: a few seconds to connect, and limits on total download time and page size. If a site keeps failing, back off and try it again later instead of tying up a fetcher.

You: Cache address lookups. Looking up the same website’s address thousands of times is slow and puts load on DNS servers.

You: And since each site has its own queue, a slow site only delays its own pages. Everything else keeps moving. (Lesson 6.1 covers timeouts and back-off.)

The complete architecture

Here’s everything together: the full design from the last stage, with every part we added along the way. If you only remember one picture from this question, make it this one.

Seed URLsURL frontierpriority + one queue per siteFetchersmany, by siteDNS + robots cacheWebsitesPage storeobject storageParsersextract linksSeen URLs + fingerprintsBloom filterPage metadatawhen, hash, next crawlRe-crawl schedulerby how often pages changenext URLlook upGET pagesave HTMLnew links?unseen linksrecorddue again
54%

How one request travels through it:

  1. Seed URLs go into the frontier, the list of URLs still to visit. It keeps one queue per website and remembers when each site may be visited next.
  2. A fetcher asks for its next URL. It only gets one from a site that’s ready, so no site is hit too often.
  3. The fetcher looks up the site’s address and robots.txt rules from a cache, then downloads the page with a short timeout.
  4. It saves the HTML to object storage and records the fetch time and a fingerprint of the content.
  5. A parser reads the page, pulls out the links, and checks each against the seen-URL filter. Only new ones go back to the frontier.
  6. A scheduler puts pages back into the frontier when they’re due for a re-crawl, sooner for pages that change often.

How this answer gets scored

Interviewers don’t tick boxes, but their notes on this question tend to land on the same few moments. Here’s what each one sounds like from the other side of the table.

MomentWhat earns creditWhat costs you
The first five minutesAsking about politeness, duplicates and freshnessAsking only how many pages
The numbersSeeing that politeness, not speed, is the limitSizing machines for bandwidth and stopping
The frontierA queue per site with a “not before” timeOne shared first-in-first-out queue
RepeatsCleaning URLs and using a Bloom filterA database lookup for every link
TrapsDepth limits and per-site page budgetsNot mentioning them
FreshnessRe-crawl by change rate, with “only if changed” requestsRe-crawling everything weekly

What the left column has in common: every choice points back to a requirement or a number from earlier. If you can say where each box came from, the interviewer will notice.