A crawler is a breadth first search over a graph with no map. The nodes are servers owned
by people who did not ask you to visit. Some of the edges are traps built to keep you
walking forever. And the graph is larger than your storage.
Web crawler / at a glance
It is also the design question most likely to reveal whether someone thinks about being a
good citizen on shared infrastructure, which is why interviewers like it.
"Crawl the web" is not a requirement. These six answers turn it into one, and two of them
are about being a guest on other people's servers rather than about scale.
You ask
They say
What it settles
How many pages, and in what time?
A billion pages a month, refreshed continuously.
About 400 fetches a second sustained, which sounds small until you remember it has to be spread politely across millions of hosts.
Do we obey robots.txt and per host rate limits?
Always. Getting blocked is worse than being slow.
The frontier design. Politeness is per host, so the queue cannot be a single queue, and this is the answer that forces the whole structure.
HTML only, or JavaScript rendered pages too?
HTML for now.
A fetcher stays cheap. Rendering needs a headless browser per page, which is roughly a hundred times the CPU, and it would change the machine count by two orders of magnitude.
How fresh do already crawled pages have to be?
News in minutes, most pages in weeks.
Recrawl is a priority problem, not a loop. It also justifies keeping etags and content hashes so an unchanged page costs a 304 rather than a download.
What do we do with what we fetch?
Store the raw page and hand it to an indexing pipeline.
The crawler writes to object storage and a queue, and owns nothing downstream. Say this or you will end up designing a search engine by accident.
How do we know we have seen a page before?
By URL, and by content, since the same page has many URLs.
Two separate filters. URL dedupe stops the loop, content dedupe stops you storing the same article forty times under forty tracking parameters.
Fetch pages starting from a seed list and follow links. Breadth first, so shallow pages are found before deep ones.
Never overload a single site. Politeness is a hard requirement, not an optimisation.
Avoid fetching the same content twice. Both the same URL and the same content under different URLs.
Recrawl pages as they change. A crawl that runs once is a snapshot, not a crawler.
The numbers you commit to
One billion pages a month.
Respect robots.txt and a per host rate limit at all times.
Survive a machine dying without losing or re-fetching its work.
Storage grows for years, so the design has to assume it never all fits in memory.
Cut, and say so out loud
Ranking and the search index. This produces the raw pages, and building the inverted index is its own question.
JavaScript rendering. Worth one sentence: a headless browser is roughly two orders of magnitude more expensive per page, so it runs as a separate tier for a small subset of URLs.
Parsing quality, boilerplate removal and language detection.
Politeness caps you at 1 fetch per second per host, so hitting 386 pages a second means having roughly 386 different hosts in flight simultaneously. That is why the frontier is organised by host rather than as one big queue: the constraint is per host, so the data structure has to be per host too.
That last line is the whole insight. Everyone starts by imagining one queue of URLs. The
politeness rule makes that structure wrong before you write any code.
The etag and content hash are the recrawl budget. Pages that never change get checked rarely and with a conditional request, so almost all of your fetch capacity goes to pages that actually move.
1when, and what nextnext URL2allowed?resolveraw page34extracted links5bodyrecord6hand offnew URLs only7Seed URLsand sitemapsURL frontierqueue per hostSchedulerpoliteness + priorityFetchersx500, statelessrobots.txtcached per hostDNS cacheown resolverParserextract linksContent storeobject storageSeen filterURL hashesNear duplicatecontent hashURL stateetag, last crawledIndexing queuesomebody else
Figure 1. It is a loop, not a pipeline. Everything hard lives in two boxes: the frontier decides what to fetch next and when, and the seen filter stops the loop from running forever.
Walking Figure 1:
Seeds and sitemaps prime the frontier. After the first hour they contribute almost
nothing, because the crawl feeds itself.
A fetcher asks for work and gets a URL whose host is due. It never picks its own work,
which is what makes politeness possible across 500 machines.
Before the fetch: robots.txt from a per host cache, and DNS from your own resolver.
Both matter more than they look. Uncached DNS at 400 lookups a second will get you
throttled by your resolver before the web ever notices you.
The raw page goes to object storage exactly once, and a pointer goes to the indexing
queue. The crawler's job ends there.
The parser pulls links out.
Every extracted URL is normalised and checked against the seen filter, and the page
body is checked against the near duplicate filter. Whatever survives updates the URL
state table with an etag and a crawl time.
New URLs go back to the frontier and the loop closes.
The loop is the design: fetch, store, extract, filter, enqueue. Everything hard lives in
two of those boxes, the frontier and the seen filter, and the rest is plumbing.
Your answer
Step 2 says a fetcher never picks its own work. What goes wrong if all 500 machines just take the next URL off one shared queue?
Two properties have to hold at once, and they pull against each other. Politeness says
never fetch from one host too quickly. Priority says important pages should be crawled
sooner. A single queue gives you neither.
scoreNew URLsPriority queuesby importanceHost queuesone per hostSchedulenext allowed time
scoreNew URLsPriority queuesby importanceHost queuesone per hostSchedulenext allowed time
1/4 A new URL is scored and dropped into a priority band. Importance can be as simple as depth from the seed, or as involved as a PageRank estimate. This is the layer that decides what gets crawled sooner.
The frontier does not fit in memory. A billion URLs at a couple of hundred bytes is
hundreds of gigabytes, so host queues live on disk with only the heads cached, which is a
normal and boring thing to say out loud.
Politeness is not one number
robots.txt can specify a crawl delay, and it must be fetched and cached per host before
anything else on that host. Sites also vary enormously in what they can take: a large
site may be fine with ten requests a second while a small one on shared hosting is hurt
by one. Adapting the rate from observed response times and error rates is what separates
a crawler people tolerate from one that gets blocked.
There are two different duplicate problems and they need different tools.
The same URL. A billion URLs is far too many to keep in a set in memory. Hash each URL
to 64 bits and keep it in a bloom filter, which answers "definitely new" or "probably
seen" in constant space. False positives mean occasionally skipping a page you have not
crawled, which for a crawler is an acceptable loss. False negatives do not happen, which is
the property that matters.
The same content under different URLs. Session ids in query strings, print versions,
trailing slashes, and the same article on five country domains. URL normalisation catches
the mechanical cases: lowercase the host, drop the fragment, sort query parameters, strip
known tracking parameters. For genuinely duplicated content, hash the document with a
similarity preserving hash such as simhash, so near duplicates collide and not just exact
ones.
Crawler traps. An infinite calendar that always has a next month. A faceted search with
every filter combination as a URL. These are not malicious, just infinite. Cap depth, cap
URLs per host, and watch for hosts producing large numbers of near identical pages.
Partition the frontier by host, not by URL. This falls out of politeness: all URLs for one
host must be handled by one worker, or two workers will hit that host simultaneously and
no rate limiter will save you.
Consumer group
part 0
lag 4consumer 0
part 1
lag 3consumer 1
part 2
lag 5consumer 2
part 3
lag 4consumer 0
part 4
lag 3consumer 1
part 5
lag 4consumer 2
Hosts hash to partitions, and each partition has one owner. Politeness is guaranteed structurally, because there is only ever one worker fetching a given host. Work is roughly even as long as hosts are roughly similar in size.
Switch to the last one and note that it has the best numbers. That is the point: the
metric you would naturally optimise says the wrong design is better.
politeno per host limittrapfilter saturatedbounded
fetchFrontierFetchersx500Target sitesSeen filter
fetchFrontierFetchersx500Target sitesSeen filter
Healthy. One fetch per host per second, robots.txt cached and respected, 400 pages a second across hundreds of thousands of hosts. Nobody notices you, which is the goal.
Read every step as text
polite · Healthy. One fetch per host per second, robots.txt cached and respected, 400 pages a second across hundreds of thousands of hosts. Nobody notices you, which is the goal.
no per host limit · Down. Take URLs from one shared queue and 500 fetchers can land on one host together. To that site you are a denial of service attack. You get rate limited, then blocked by IP, then blocked by user agent, and eventually you are in a blocklist other sites subscribe to.
trap · Down. A calendar with an infinite next month. The frontier fills with millions of URLs from one host that all differ by one parameter and contain nothing. Your crawl is now spending its whole budget generating URLs for itself.
filter saturated · Degraded. The bloom filter was sized for the URLs you expected. Past that, the false positive rate climbs and you silently start skipping pages you have never crawled. Nothing errors. Size it for the target and monitor the fill ratio.
bounded · Healthy. Per host scheduling, a depth cap, a per host URL cap, and a filter sized with headroom. The crawl is slower than it theoretically could be, and it can run for years without anyone blocking it or it eating itself.
You have 500 fetcher machines and a queue of URLs. What is wrong with each machine taking the next URL?
The opening, and it is testing exactly one thing.
You
Politeness. A popular host appears many times in that queue, so several fetchers end up hitting it at the same moment and to that site we are an attack. The rate limit that matters is per host, so the frontier has to be organised per host: a queue per host, one owner per queue, and a schedule holding the earliest time each host may next be contacted.
Goes from the symptom to the data structure in one move. The phrase "the rate limit that matters is per host" is the whole answer.
Interviewer
How do you know whether you have seen a URL before, at a billion URLs?
A scale question with a specific expected tool.
You
A bloom filter over the hashed normalised URL. It cannot say a new URL is old incorrectly in the direction that matters: no false negatives, so I never re-crawl something I have seen. False positives mean occasionally skipping a page I have not crawled, which for a crawler is cheap. I would normalise first, since sorting query parameters and stripping tracking parameters removes a large share of the duplicates before hashing.
States which error direction is acceptable and why. That is the part that shows the tool is understood rather than recalled.
Interviewer
What stops the crawl running forever on one site?
Traps. There is more than one answer and they want to hear the bounding instinct.
You
Caps, several of them. Maximum depth from the seed, maximum URLs per host per pass, and a watch for hosts producing large numbers of near identical pages, which is what an infinite calendar or a faceted search looks like from outside. None of those need the site to be malicious, they just need it to be infinite, which plenty are.
Multiple independent bounds rather than one clever detector. That is the right instinct for adversarial input.
Interviewer
How would you decide when to recrawl a page?
Open, and it separates a one shot crawler from a real one.
You
From observed change. Store a content hash and an etag per URL, and send a conditional request: a 304 costs almost nothing. Pages that come back unchanged several times get backed off exponentially, pages that change every visit get crawled more often. I would not set a fixed interval, since the distribution of change rates on the web spans several orders of magnitude and any single number is wrong for almost every page.
Adaptive from measurement rather than a constant, and says why a constant cannot work.
1. Why partition the frontier by host rather than by URL, when partitioning by URL balances better?
2. Your bloom filter reports a URL as seen when it has never been crawled. What is the consequence?
3. A site returns a 304 Not Modified for most of your recrawls. Is that a problem?
Worth memorising
A billion pages a month is about 400 fetches a second, spread politely across millions of hosts.
Politeness is per host. That one fact is why the frontier is many queues rather than one.
A 64 bit URL hash plus a Bloom filter answers "seen it?" without touching a database.
A conditional request that returns 304 is the cheapest recrawl there is. Spend your fetch budget on pages that move.
Say this in 60 seconds
A crawler is a breadth first traversal where the binding constraint is politeness, so the frontier is not one queue, it is a queue per host with one owner each and a schedule holding the earliest time each host may next be contacted. That structure makes it impossible for two workers to hit a host at once, which a shared queue plus a rate limiter cannot guarantee. Deduplication is two problems: the same URL, handled by normalising and then a bloom filter over the hash, where false positives just mean occasionally skipping a page, and the same content under different URLs, handled by a similarity preserving hash of the body. I would cap depth and URLs per host to survive infinite calendars and faceted search, which are not malicious, just endless. Recrawl is driven by observed change using etags and content hashes, backing off pages that never move, because change rates on the web span orders of magnitude and any fixed interval is wrong for nearly every page.