Skip to the solution
HardSystem Design15 min read

How would you design a Web Crawler?

0 views

The problem

Question presented to candidate: "We need to build a crawler that downloads about a billion web pages a month for a search index. Walk me through the architecture. How do you stay polite to websites, avoid downloading the same thing twice, and not get stuck?"

What a strong answer should cover:

  • A worked capacity estimate: pages per second, bandwidth, storage per month, and the number of hosts that must be crawled in parallel because of per-host politeness
  • The pipeline: seeds, URL frontier, DNS resolution, fetchers, parser and link extractor, deduplication, content store, with the frontier as the central data structure
  • Politeness: one connection and a delay per host, robots.txt (standardised as RFC 9309; Crawl-delay is not part of it), and a frontier with priority queues in front and per-host queues behind, as in the Mercator design
  • Deduplication at two levels: normalised URLs (a seen set or Bloom filter) and page content (exact fingerprints, near-duplicate detection with SimHash)
  • Crawler traps, per-host budgets and depth limits; recrawl scheduling by change rate with conditional GETs; partitioning the frontier by host across machines

Clarifying questions expected:

  • What is the crawl for (search index, price monitoring, archive), and so which pages matter most?
  • How fresh must the data be: one pass, or continuous recrawling?
  • Do we need to render JavaScript, or is raw HTML enough?

Code / implementation expected: Optional β€” a URL frontier with per-host delays, robots rules and dedup; the example simulates exactly that, including a crawler trap.

  • system-design
  • case-study

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

15 min read Β· 15 sections

Read the code

Crawler frontier: per-host politeness, robots.txt, URL normalization, content dedup and a crawler trap
// Web crawler core: URL frontier with per-host politeness, robots.txt, URL and content dedup, trap limits.
const crypto = require("crypto");

console.log("== capacity estimate ==");
const pagesPerMonth = 1000000000, kbPerPage = 100, delaySec = 1;
const pagesPerSec = Math.ceil(pagesPerMonth / (30 * 86400));
console.log("pages/s: " + pagesPerSec + ", download: " + (pagesPerSec * kbPerPage) / 1000 + " MB/s, raw storage/month: " + (pagesPerMonth * kbPerPage) / 1000000000 + " TB");
console.log("at 1 request per " + delaySec + " s per host, hosts that must be in flight at once: >= " + pagesPerSec * delaySec);

// The simulated web: url -> { links, text }. c.com/calendar links to the next day forever (a crawler trap).
const web = {
  "http://a.com/": { links: ["http://A.com/news#top", "http://a.com/private/admin", "http://b.com/", "http://a.com/about/"], text: "a home" },
  "http://a.com/news": { links: ["http://a.com/news/1", "http://a.com/news/2", "http://c.com/copy"], text: "a news" },
  "http://a.com/news/1": { links: ["http://a.com/"], text: "story one" },
  "http://a.com/news/2": { links: ["http://a.com/news"], text: "story two" },
  "http://a.com/about": { links: [], text: "about a" },
  "http://b.com/": { links: ["http://b.com/shop", "http://c.com/calendar?day=1"], text: "b home" },
  "http://b.com/shop": { links: ["http://b.com/"], text: "b shop" },
  "http://c.com/copy": { links: [], text: "  Story ONE " },
};
for (let d = 1; d <= 50; d++) web["http://c.com/calendar?day=" + d] = { links: ["http://c.com/calendar?day=" + (d + 1)], text: "day " + d };
const robots = { "a.com": { disallow: ["/private"], delayMs: 1000 }, "b.com": { disallow: [], delayMs: 2000 }, "c.com": { disallow: [], delayMs: 1000 } };
const MAX_PER_HOST = 5, FETCH_MS = 100;

function normalize(url) {
  let u = url.split("#")[0];
  const m = /^(\w+):\/\/([^/]+)(.*)$/.exec(u);
  let path = m[3] || "/";
  if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
  return m[1].toLowerCase() + "://" + m[2].toLowerCase() + path;
}
const hostOf = (url) => /^\w+:\/\/([^/]+)/.exec(url)[1];
const pathOf = (url) => url.slice(url.indexOf("/", url.indexOf("//") + 2)) || "/";
const fingerprint = (text) => crypto.createHash("sha256").update(text.trim().toLowerCase()).digest("hex").slice(0, 8);

function crawl(polite) {
  const queues = new Map(), nextAllowed = new Map(), seenUrls = new Set(), seenContent = new Map(), perHost = new Map();
  const stats = { fetched: 0, robots: 0, dupUrl: 0, dupContent: 0, budget: 0 };
  const lines = [], times = [];
  let clock = 0;
  function add(raw) {
    const url = normalize(raw), host = hostOf(url);
    if (seenUrls.has(url)) { stats.dupUrl++; return; }
    seenUrls.add(url);
    if (robots[host].disallow.some((p) => pathOf(url).startsWith(p))) { stats.robots++; return; }
    if (!queues.has(host)) { queues.set(host, []); nextAllowed.set(host, 0); }
    queues.get(host).push(url);
  }
  add("http://a.com/");
  for (;;) {
    let best = null;
    for (const [host, q] of queues) if (q.length && (best === null || nextAllowed.get(host) < nextAllowed.get(best))) best = host;
    if (best === null) break;
    const url = queues.get(best).shift();
    if ((perHost.get(best) || 0) >= MAX_PER_HOST) { stats.budget++; continue; }
    if (polite) clock = Math.max(clock, nextAllowed.get(best));
    const page = web[url];
    times.push([best, clock]);
    perHost.set(best, (perHost.get(best) || 0) + 1);
    stats.fetched++;
    const fp = fingerprint(page.text);
    if (seenContent.has(fp)) { stats.dupContent++; lines.push("t=" + clock + "ms " + url + " duplicate of " + seenContent.get(fp)); }
    else { seenContent.set(fp, url); lines.push("t=" + clock + "ms " + url + " stored, links found: " + page.links.length); for (const l of page.links) add(l); }
    clock += FETCH_MS;
    nextAllowed.set(best, clock + (polite ? robots[best].delayMs : 0));
  }
  let worst = 0;
  for (const [h, t] of times) worst = Math.max(worst, times.filter(([h2, t2]) => h2 === h && t2 >= t && t2 < t + 1000).length);
  return { lines, stats, worst, clock };
}

const run = crawl(true);
console.log("== polite crawl ==");
for (const l of run.lines) console.log(l);
const s = run.stats;
console.log("fetched " + s.fetched + ", blocked by robots.txt " + s.robots + ", duplicate URLs skipped " + s.dupUrl + ", duplicate content " + s.dupContent + ", cut by per-host budget " + s.budget);
console.log("finished at t=" + run.clock + "ms, max fetches to one host in any 1 s window: " + run.worst);
const naive = crawl(false);
console.log("== same crawl with no politeness delay ==");
console.log("finished at t=" + naive.clock + "ms, max fetches to one host in any 1 s window: " + naive.worst);

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 94 of 106 in System design

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextHard

How would you design a unique ID generator at scale?

Continue