Hiring BarSupport

Design a Web Crawler — Staff-Level Case Study

Case study73 min read8 diagrams

Technologies referenced in this case study: Kafka · Cassandra · Redis · Elasticsearch · DynamoDB

How to Use This Case Study#

Organized for interview use first, reference second. Read front-to-back once, then return to individual sections for targeted review.

ModeTimeWhat to Read
Quick Review15 minExecutive Summary → Interview Walkthrough → Fault Lines table → Drills 1, 3, 5
Targeted Study1–2 hrsExecutive Summary → Walkthrough → Section 3 (Fault Lines) → Section 4 (Failure Modes) → weak-spot Deep Dives
Deep Dive3+ hrsEverything, including The Principal Lens and the appendices
What is a Web Crawler? — Why interviewers pick this topic

A web crawler starts from a set of seed URLs, fetches pages, extracts links, and repeats — building a corpus for a search index, an archive, a training dataset, or a change-monitoring product. The fetch loop is 30 lines of Python. Running it against ~10⁹–10¹¹ URLs owned by ~10⁸ independent site operators, none of whom asked to be crawled, is the actual problem.

Before vs After — the "politeness is optional" launch:

Without per-host politeness:
t=0:      New crawler launched, 2,000 fetchers, frontier ordered by PageRank only
t=+5min:  Top 50 hosts each receive 300-800 req/s from our IP range
t=+12min: A mid-size news site's origin falls over; their on-call traces it to us
t=+40min: Our /16 is blocked by two CDNs; 18% of fetches now return 403
t=+1day:  Abuse complaints to our cloud provider; legal gets an email
t=+3days: Freshness for the top 10K hosts is WORSE than before launch

With host-partitioned politeness:
t=0:      Same launch, same 2,000 fetchers
t=+5min:  Each host capped at 1 concurrent connection, >= 1s between fetches
t=+5min:  Fetchers stay busy by interleaving ~500K distinct hosts
t=+1hr:   Adaptive delay backs off hosts whose latency or 5xx rate rises
t=+1day:  Zero complaints; aggregate throughput identical; ban rate < 0.1%

Why interviewers reach for this question: It looks like a BFS exercise, so it cleanly separates candidates who design a data structure from candidates who design a good citizen with a budget. The hard parts are the frontier (what to fetch next, under per-host constraints), deduplication at 10¹⁰ scale, freshness scheduling, and adversarial input — spider traps, infinite calendars, 2 GB "HTML" responses. Every one of those has an owner and a victim.

Mechanics Refresher: Frontier and Dedup Options
MechanismHow It WorksProsCons
FIFO BFS queueOne global queue, pop and fetchTrivialNo priority, no politeness; one host with 10M links starves the crawl
Priority queueScore URLs (PageRank, freshness need), pop highestCrawls important pages firstStill no politeness — top pages cluster on few hosts
Mercator-style front/back queuesFront queues by priority; back queues one-per-host; heap keyed by host's next-allowed timePriority and politeness; the standard designMore state; needs host→worker partitioning
URL-seen Bloom filter~10 bits/URL for 1% FPR10B URLs in ~12.5 GB RAMFalse positives silently skip pages; cannot delete
URL-seen KV storeExact set in RocksDB/Cassandra keyed by URL hashExact, deletable, carries metadataDisk seek per lookup unless batched
Content hash (SHA-256)Exact duplicate detectionCheap, exactMisses near-dups (ads, timestamps differ)
SimHash (64-bit)Locality-sensitive fingerprint; near-dup if Hamming distance ≤ 3Catches boilerplate-mutated copiesTuning-sensitive; permuted-table index needed at scale

For most production systems: Mercator-style front/back queues partitioned by host, an exact URL-seen store with a Bloom filter in front of it, SHA-256 for exact dedup and SimHash for near-dup. The data structures are not the interview — the scheduler policy, politeness contract, and trap defenses are.


Executive Summary

If you only read one section, read this. Everything in the case study flows from the contrast below.

What This Interview Actually Tests#

A web crawler is not a graph traversal question. Everyone can write BFS.

It is a scheduling-under-external-constraints question that tests:

  • Whether you clarify what the corpus is for before sizing anything
  • Whether you treat other people's servers as a constrained, shared resource you do not own
  • Whether you can defend against adversarial and pathological input at 10⁹ scale
  • Whether you can own freshness as an SLO rather than a hope

The key insight: The throughput of a crawler is bounded not by your fetchers but by politeness — the number of distinct hosts you can interleave. Staff engineers design the frontier around the host, not the URL, and they name who gets hurt when the crawler misbehaves: the site operator first, your IP reputation second, your index freshness third.

The L5 vs L6 Contrast — Start Here#

BehaviorSenior (L5)Staff (L6)Principal (L7)
First moveDraws seed → queue → fetcher → parser → storeAsks "Is this a freshness crawler for search, a one-shot corpus crawl, or change monitoring?"Asks who else in the org crawls, and whether one crawl platform should serve search, ML data, and trust & safety
FrontierDistributed priority queue of URLsFront queues by priority, back queues by host, partitioned by hash(host) so politeness is single-writerMakes the politeness contract an org-wide policy with legal sign-off and a published bot identity
Dedup"Bloom filter on URLs"Separates URL-seen, exact-content, and near-dup; names the false-positive victim (pages never crawled)Prices the dedup tier: storage $ vs wasted fetch bandwidth vs index quality
Freshness"Recrawl every N days"Per-URL change-rate estimation; recrawl budget allocated by importance × change probabilityTreats freshness as a product SLO per tier with a monthly recrawl budget negotiated with search leadership
Failure"Retry failed fetches"Spider traps, host bans, DNS saturation, frontier explosion — each with a metric and ownerDesigns the org's exposure: abuse-complaint SLAs, IP pool isolation per crawl purpose, regulatory takedown path
OwnershipCrawler team owns everythingCrawler owns fetch + frontier; index team owns ranking signals that feed priority; legal owns robots/ToS policyRedraws boundaries: crawl-as-a-platform with per-consumer quotas and chargeback
Why "first move" separates levels

L5: Starts with the pipeline. Competent — every crawler has that pipeline — but it assumes the goal. A search crawler optimizes freshness of important pages; an archive crawl optimizes coverage per dollar; a price monitor optimizes time-to-detect-change on a known set of 5M URLs. These produce different frontiers, different storage, and different failure budgets.

L6: "Before sizing, what's the corpus for? If it's a search index, freshness of the top 1% of pages dominates and I'll spend most of the design on recrawl scheduling. If it's a one-shot corpus for training data, I'd optimize for dedup and throughput and barely recrawl. I'll assume a general web search crawler — 1B pages refreshed monthly, with the top 10M pages refreshed daily."

Why "frontier" separates levels

L5: A distributed priority queue in Redis or Kafka. It works until the top-scored 10,000 URLs all live on wikipedia.org and 2,000 fetchers hammer one host — or, if you rate-limit at fetch time, 1,999 fetchers sit idle waiting on one host's delay.

L6: The unit of scheduling is the host. Partition the frontier by hash(host) so exactly one worker owns each host's queue, its robots.txt, its crawl delay, and its adaptive backoff. Politeness becomes a local property with no distributed lock. Priority lives inside each host queue and in choosing which host is next.

Why "failure" separates levels

L5: Retries with backoff for 5xx and timeouts. Correct and necessary.

L6: The dangerous failures are not errors — they are successes. A calendar page that returns 200 for every future month. A site that serves the same article under 40,000 session-ID URLs. A host that returns 200 with a "you are blocked" body. None of these trip an error-rate alarm; all of them silently burn crawl budget. The Staff answer names the detection metric for each: per-host URL growth rate, per-host dup ratio, per-host content-length entropy.

The Staff Positions#

PositionRationale
Partition the frontier by host, not by URLPoliteness becomes single-writer and local; no distributed rate limiter on the hot path
Politeness is a hard constraint, throughput is the objectiveA banned crawler has zero throughput; a complaint costs more than a day of crawl
Exact URL-seen store, Bloom filter only as a cache in frontBloom false positives are silent coverage loss nobody will ever detect
Budget per host, not just per crawlCaps the damage of any single spider trap at the host's budget (e.g., 50K URLs/day)
Recrawl scheduling by estimated change rate × importanceUniform recrawl wastes ~60–80% of fetches on pages that didn't change
Store raw fetched bytes (WARC) before parsingParsers change weekly; refetching 1B pages to re-parse costs a month of politeness budget
Crawler identity is public and honestDocumented User-Agent, verify-via-reverse-DNS, contact URL — the cheapest ban-avoidance there is

The Three Intents#

IntentConstraintStrategyFailure ModeCorrectness Bar
Search-index freshness crawlImportant pages must be fresh (hours for news, days for the head, weeks for the tail)Continuous crawl; change-rate estimation; priority = importance × P(changed)Silent staleness: index serves yesterday's prices and dead linksFreshness SLO per tier, e.g. p90 age < 24h for top 10M URLs
Bulk corpus crawl (archive / ML data)Maximum unique, high-quality coverage per dollarBatch crawl; aggressive near-dup removal; little or no recrawlCorpus polluted by spam, dups, traps; licensing/opt-out violationsDup rate < 5%, opt-out compliance 100%, provenance per document
Targeted change monitoringDetect changes on a known set (prices, filings, competitor pages) fastFixed URL set; frequent conditional GETs; diff extractionMissed change or false change alert from dynamic noiseTime-to-detect p95 < N minutes on the watched set

🎯 Staff Move: "I'll assume a general search-index crawler: roughly 1B pages kept fresh on a monthly cycle, with the top 10M refreshed daily and news hosts refreshed hourly. That puts the hard design in two places — the host-partitioned frontier that enforces politeness, and the recrawl scheduler that decides which 40M fetches a day are worth spending. Archive and change-monitoring crawls reuse the fetch layer but need a different scheduler."

The Five Fault Lines#

#Fault LineThe Tension
1Politeness vs ThroughputFetch aggressively (fresher index, angry sites) or conservatively (good citizen, slower crawl)?
2Freshness vs CoverageSpend the fetch budget recrawling known pages or discovering new ones?
3Centralized vs Host-Partitioned FrontierOne global priority queue (simple, contended) or per-host ownership (scalable, rebalancing pain)?
4Dedup Precision vs CostExact sets (RAM/disk heavy) or probabilistic filters (cheap, silently lossy)?
5Crawl Everything vs Crawl ResponsiblyMaximize corpus (ignore opt-outs, ToS, PII) or constrain it (legal safety, smaller corpus)?

In the Wild: Real Production Systems#

Why this section belongs here: Citing specific, publicly documented crawlers shows you have studied operational reality, not just a textbook BFS.

Mercator (Compaq/DEC research) — The Front-Queue/Back-Queue Frontier#

Heydon and Najork's Mercator paper (1999) introduced the frontier design most production crawlers still resemble: front queues encode priority, back queues hold one host each, and a heap keyed by "earliest next fetch time per host" decides which back queue a fetcher thread serves. It also documented a URL-seen test with an in-memory cache in front of a disk-resident set, and content fingerprinting to skip duplicate documents.

Staff insight: Mercator's contribution was not speed — it was making politeness a structural property of the queue rather than a check in the fetcher. That is the sentence to say in an interview.

Google — Near-Duplicate Detection with SimHash#

Manku, Jain, and Das Sarma (WWW 2007) described using 64-bit SimHash fingerprints over a multi-billion-page crawl, treating pages within Hamming distance 3 as near-duplicates, and building permuted fingerprint tables so the lookup stays fast. Google also documents that Googlebot caches robots.txt for up to 24 hours and backs off when a site returns 5xx or 429.

Staff insight: Near-dup detection is a budget mechanism, not a storage-saving trick. Every near-dup you fetch is a fetch you didn't spend on fresh content — dedup protects freshness.

Common Crawl — The Open Bulk Corpus#

Common Crawl publishes monthly-ish crawls of a few billion pages each as WARC files on public cloud object storage, respecting robots.txt with an identifiable CCBot user agent. It is the canonical example of the bulk corpus intent: little recrawl scheduling, heavy emphasis on coverage, provenance, and a stable archival format that downstream consumers (including many ML training pipelines) re-parse independently.

Staff insight: Storing raw bytes in a standard format decouples the crawl from every consumer's parser. When the "consumer" becomes 30 teams, that decoupling is the platform.

What Interviewers Probe#

After You Say...They Will Ask...(What They're Evaluating)
"Priority queue of URLs""The top 10K URLs are all on 5 hosts. Now what?"Do you see politeness as structural?
"Bloom filter for dedup""What's the false-positive rate, and who notices when a page is never crawled?"Do you name silent failure victims?
"Recrawl every 7 days""The news homepage changes every 5 minutes; the 2009 blog post never changes. Same schedule?"Can you allocate a budget?
"We respect robots.txt""robots.txt fetch times out. Crawl or don't crawl?"Fail-open vs fail-closed with an owner
"Scale fetchers horizontally""What actually bounds throughput?"Hosts × politeness delay, DNS, bandwidth — not fetcher count
"Store the parsed text""Parser had a bug for 3 weeks. How do you fix the corpus?"Raw-bytes retention; reprocessing vs refetch

System Architecture Overview#

Diagram: System Architecture Overview

Reading the diagram: The frontier is partitioned by host so a single worker owns each host's queue, robots.txt, and crawl delay — politeness needs no coordination. Fetchers write raw bytes to object storage before parsing, so a parser bug is a reprocessing job, not a month of refetching. The URL-seen store closes the loop: only never-seen URLs re-enter the frontier. The recrawl scheduler is the only component that decides which known pages to spend budget on — it is where freshness is won or lost.

Quick-Reference: The 30-Second Cheat Sheet#

TopicThe L5 AnswerThe L6 Answer — Say This
Frontier"Distributed priority queue""Front queues for priority, back queues per host, partitioned by hash(host). Politeness is single-writer."
Politeness"Rate limit each domain""One connection per host, ≥1s spacing, adaptive backoff on latency and 5xx/429, robots.txt cached 24h. Throughput comes from interleaving 500K hosts."
Dedup"Bloom filter""Three layers: URL-seen (exact store, Bloom as cache), exact content (SHA-256), near-dup (SimHash, Hamming ≤ 3)."
Freshness"Recrawl weekly""Estimate per-URL change rate; spend the recrawl budget on importance × P(changed). Conditional GET with ETag/Last-Modified saves ~50% of bytes on unchanged pages."
Traps"Max depth limit""Per-host URL budget, URL-pattern novelty scoring, path-repetition detection, and a trap-suspect review queue."
Storage"Store parsed text in a DB""Raw WARC in object storage first. Parsing is a replayable downstream job."

Key Numbers Worth Memorizing#

MetricValueWhy It Matters
1B pages / 30 days~385 fetches/s averageModest — the constraint is host interleaving, not raw rate
Average HTML page (raw)~100 KB; ~20–30 KB gzip1B pages ≈ 100 TB fetched, ~25 TB stored compressed
Per-host politeness default1 connection, ≥ 1 s between requests86,400 fetches/host/day max; the top host can't be crawled faster
Fetch latencyp50 ~300 ms, timeout 30 sAsync I/O: ~1K concurrent connections per fetcher node
DNS lookup (uncached)10–200 msWithout a local cache, DNS becomes the bottleneck at ~1K fetch/s
robots.txt cache TTLup to 24 hGoogle's documented maximum; stale longer and you ignore opt-outs
Bloom filter sizing~9.6 bits/key at 1% FPR10B URLs ≈ 12 GB; 1% FPR = 100M URLs silently never crawled
SimHash near-dup threshold64-bit, Hamming ≤ 3Publicly documented operating point at multi-billion-page scale
Pages that change weeklytypically a minority (~20–30%) of pagesWhy uniform recrawl wastes most of the budget
Conditional GET (304) payload~0.5 KB vs ~100 KBRecrawls of unchanged pages cost ~200× fewer bytes
Per-host URL budget10K–100K URLs/day defaultCaps any single spider trap's blast radius

Interview Walkthrough

The most common mistake: Candidates spend 20 minutes drawing seed → queue → fetch → parse → store and computing storage for 1B pages, then run out of time before politeness, freshness, or traps come up. Compress the pipeline to 5 minutes. The level is decided in the frontier and the scheduler.


Phase 1: Requirements & Framing (2–3 minutes)#

State the functional scope in one sentence:

"We're building a crawler that discovers and fetches web pages, extracts links, and hands content to a search indexer — continuously, not as a one-shot job."

Then spend your time on the non-functional requirements that actually shape the design:

QuestionWhy It MattersDefault I'd Assume
What is the corpus for?Freshness vs coverage vs change detectionSearch index
How many pages, how fresh?Sizes the fetch budget1B pages; top 10M daily; news hourly
Politeness contract?Hard constraint on throughputrobots.txt compliant, 1 conn/host, ≥1s spacing
Content types?Parser complexity, bytesHTML only; skip PDFs/video in v1
JavaScript rendering?10–50× CPU cost per pageNo in v1; render a targeted 1–5% later
Legal/opt-out requirements?Changes what we may storeHonor robots, noindex, takedown requests within 24h

"The number I'll design around is the recrawl budget: at ~40M fetches a day, which 40M pages are worth fetching? That's where most of the interesting tradeoffs live."


Phase 2: Core Entities & API (1–2 minutes)#

Keep this tight. The entities that matter:

Host       { host, ip_set, robots_rules, robots_fetched_at, crawl_delay_ms,
             budget_remaining_today, health_score, owner_partition }
UrlRecord  { url_hash (64-bit), canonical_url, host, first_seen, last_fetched,
             last_changed, change_rate_est, importance, etag, last_modified,
             content_hash, simhash, status }
FetchTask  { url_hash, priority_band, not_before, attempt }
FetchResult{ url_hash, http_status, headers, body_ref (WARC offset), fetched_at }

Internal APIs — not public HTTP, but name them:

frontier.enqueue(urls[], source, priority_hint)
frontier.lease(worker_id, max_hosts) -> [HostBatch]
fetch.result(FetchResult)                    // async, to Kafka
policy.check(host) -> {allowed, delay_ms, budget}
scheduler.recrawl_candidates(partition, n)   // daily or hourly

"The key identity decision: canonical URL after normalization, hashed to 64 bits. Two URLs that normalize the same are the same page for dedup, even if the server disagrees."


Phase 3: High-Level Architecture (≤5 minutes)#

Staff candidates spend under 5 minutes here. Draw the loop, name the partitioning key, move on.

Diagram: Phase 3: High-Level Architecture (≤5 minutes)

Say it in four sentences:

  1. "Frontier is partitioned by hash(host) — each partition owner holds the host's queue, robots.txt, crawl delay, and budget."
  2. "Fetchers lease batches of URLs per host, fetch asynchronously with a local DNS cache, and write raw bytes to object storage before anything parses them."
  3. "Parsing, link extraction, and dedup are downstream stream processors; new URLs go through the URL-seen store and re-enter the frontier on their host's partition."
  4. "A separate recrawl scheduler injects known URLs based on estimated change rate and importance — that's the freshness engine."

Phase 4: Transition to Depth (1 minute)#

The sentence that steers the interviewer:

"The pipeline is the easy part. The three places I'd like to go deep are: how the frontier enforces politeness without a distributed lock, how the recrawl scheduler spends a fixed budget to maximize freshness, and how we defend against spider traps and duplicate content that silently eat that budget. Which is most interesting to you?"

If they don't pick: go frontier → traps → freshness. The frontier is the architectural core; traps show operational ownership; freshness shows product judgment.


Phase 5: Deep Dives (25–30 minutes)#

Deep dive 1 — The host-partitioned frontier (8–10 min).

  • Partition key hash(registered_domain) or hash(host). Registered domain (eTLD+1, via the Public Suffix List) is safer: a.example.com and b.example.com often share one origin server.
  • Within a partition, each active host has a back queue. A min-heap keyed on next_fetch_at picks the next ready host. After a fetch, next_fetch_at = now + max(crawl_delay, k × last_response_time) with k ≈ 10 — slow servers automatically get more spacing.
  • Front queues (priority bands 0–7) feed back queues; a biased random selector favors high bands without starving low ones.
  • Fetchers are stateless and lease HostBatches (e.g., up to 20 URLs for one host, served sequentially on one keep-alive connection).
  • Rebalancing: partitions map to workers via consistent hashing; a worker failure moves its hosts, and the new owner must re-fetch robots.txt state from the shared cache before fetching.
Diagram: Phase 5: Deep Dives (25–30 minutes)

"The throughput math is: fetches/s ≤ active_hosts / avg_delay. With 1s delay and 500K active hosts, the ceiling is 500K/s — far above our 385/s need. So the constraint isn't the head of the web; it's that the top 1,000 hosts hold maybe 10% of the URLs we care about and each can only take ~86K fetches a day."

Deep dive 2 — Spider traps and budget defense (6–8 min).

  • Per-host daily URL budget (default 10K–100K, raised for known-large hosts like Wikipedia via an allowlist with owner).
  • URL normalization: lowercase scheme/host, drop default ports, sort query params, strip known session params (sid, jsessionid, utm_*), remove fragments.
  • Trap heuristics: path depth > 16, repeated path segments (/a/b/a/b/a/b), URL length > 2 KB, per-host new-URL rate far above the host's historical baseline, near-dup ratio > 80% within a host.
  • Suspected hosts drop to the lowest priority band and go to a review queue — never silently blocked, because false positives are how you lose a real site.

Deep dive 3 — Freshness scheduling (8–10 min).

  • Estimate per-URL change rate λ from fetch history (Poisson model: P(changed within t) = 1 − e^(−λt)).
  • Priority for recrawl = importance × P(changed since last fetch). Importance comes from link-graph signals and query logs — owned by the ranking team.
  • Use conditional GET (If-None-Match, If-Modified-Since): a 304 costs ~0.5 KB instead of ~100 KB. Sitemaps' lastmod and RSS/Atom feeds give free change hints for news hosts.
  • Hard floors: top tier never older than 24h; everything recrawled at least every 90 days so dead links get detected.

Phase 6: Wrap-Up (2–3 minutes)#

Close with constraints, evolution, and what you would build later:

"To summarize: host-partitioned frontier so politeness is structural, raw bytes stored before parsing so we can reprocess without refetching, three-layer dedup, and a recrawl scheduler that spends ~40M daily fetches on importance × change probability. What I'd build next: JavaScript rendering for the ~5% of high-value pages that need it, a crawl-as-a-platform API so ML and trust & safety teams stop running their own crawlers against the same hosts, and a freshness dashboard per tier that search leadership signs off on."


Common Timing Mistakes#

MistakeTime LostWhat to Do Instead
Back-of-envelope for storage in detail5–8 minOne line: "~100 TB fetched, ~25 TB compressed per cycle" and move on
Designing the HTML parser5 min"Parser is a library; I care that it's sandboxed and replayable"
Debating Kafka vs SQS for the frontier5 minThe frontier is a partitioned, stateful service — not a message queue
Leaving politeness to "the last 5 minutes"fatalIntroduce it in Phase 1 as a hard constraint
Never mentioning freshnesslevel-decidingA crawler for search is a freshness system

1. The Staff Lens#

1.1 Why This Problem Exists in Staff Interviews#

Crawling is one of the few classic prompts where the system's main constraint is imposed by parties you don't control. Rate limiters protect your backend; caches protect your database. A crawler must protect other people's servers, obey rules those people publish in a 30-year-old text format, and survive adversarial content they serve back. That makes it a clean test of whether a candidate designs for an ecosystem or just for a throughput number.

It also tests budget allocation. A crawler never has enough fetches. The Staff skill is deciding what not to fetch — and making sure that decision is observable and owned.

1.2 The L5 vs L6 Contrast — Visual#

Diagram: 1.2 The L5 vs L6 Contrast — Visual

1.3 The Staff Question That Cuts Through Everything#

"Which fetch are we not doing today, and who decided that?"

Every crawler is budget-constrained: by politeness, by bandwidth, by storage, by downstream indexing capacity. The question forces the design to expose its allocation policy. If the answer is "whatever the queue happens to pop next," nobody owns freshness. If the answer is "the scheduler deprioritized 12M tail pages whose estimated change probability is under 2%, per the policy search ranking signed off on," you have a system someone can run.

🎯 Staff Move: "I want to make the skip decisions explicit. Every day the scheduler emits a report — fetches allocated by tier, fetches denied by budget, hosts throttled by politeness — so when someone asks why a page is stale, we can answer from data instead of guessing."


2. Problem Framing & Intent#

2.1 The Three Intents — Explained#

Intent 1 — Search-index freshness crawl. Continuous. The corpus is mostly known; the job is keeping it current and discovering the new 1–3% that appears each day. Freshness of the head dominates user-visible quality: a stale price, a dead link in the top 3 results, a news story missing for 6 hours. The frontier is fed mostly by the recrawl scheduler, not by link discovery. Correctness bar: a per-tier freshness SLO.

Intent 2 — Bulk corpus crawl. Batch. Archives and ML training corpora want breadth, provenance, and cleanliness. Recrawl is rare; dedup and quality filtering dominate cost. Opt-out compliance is now a first-order concern — site owners increasingly publish rules specifically for AI crawlers, and a corpus that ignores them is a legal liability that outlives the crawl. Correctness bar: dup rate, opt-out compliance, and provenance per document.

Intent 3 — Targeted change monitoring. A fixed watch-list (5M product pages, 50K regulatory filings). No discovery. Latency-to-detect is the SLO, so fetch frequency per URL is high — which makes politeness harder, because you revisit the same hosts constantly. Noise suppression (ignore timestamps, ad slots, CSRF tokens) is the core algorithm. Correctness bar: time-to-detect p95 and false-change rate.

DimensionSearch FreshnessBulk CorpusChange Monitoring
Frontier fed byRecrawl scheduler (~80%) + discoveryDiscovery (~100%)Static watch-list
Dominant costRecrawl fetchesStorage + dedup computeRepeated fetches per host
Worst silent failureHead goes staleCorpus full of dups/spamMissed change
Who complainsSearch quality, usersML/research consumers, legalCustomer paying for alerts

2.2 When NOT to Build a Crawler#

SituationUse InsteadWhy
You need a few million known pages onceCommon Crawl or a commercial datasetA month of crawl engineering to rebuild a public WARC dump is waste
The data has an API or a feedThe API/RSS/sitemapStructured, sanctioned, and cheaper than parsing HTML
Partner dataDirect data feed contractCrawling a partner is how partnerships end
You need logged-in contentDon'tCredentialed scraping is a ToS and legal minefield
Change monitoring on < 1K pagesA cron job with conditional GETA distributed frontier for 1K URLs is resume-driven design

🎯 Staff Move: "Before we design this, I'd check whether a public crawl plus targeted top-up fetches covers the need. Building a general crawler is a multi-year commitment with a permanent on-call and an abuse-complaint inbox."

2.3 What the Interviewer Leaves Underspecified#

UnderspecifiedWhy It MattersWhat to Say
Freshness targetSizes the recrawl budget by 10×"I'll assume tiers: hourly for news, daily for top 10M, monthly for the rest"
Politeness contractBounds throughput"robots.txt compliance, one connection per host, adaptive delay"
JS renderingCPU cost 10–50× per page"Out of v1; a render farm for a targeted subset later"
GeographyContent varies by client IP location"Crawl from 2–3 regions; primary crawl from one"
Content typesPDFs and media multiply bytes"HTML and PDFs under 10 MB; skip media"
Legal requirementsDetermines what we keep and for how long"Honor opt-outs and takedowns within 24h; retention owned by legal"

2.4 Precise Terminology#

TermPrecise Meaning
FrontierThe set of URLs known but not yet (re)fetched, plus the scheduling state that orders them
PolitenessConstraints on request rate and concurrency per host (or per IP/registered domain)
Registered domain (eTLD+1)example.co.uk for a.b.example.co.uk; computed via the Public Suffix List
URL normalizationDeterministic rewrite so equivalent URLs map to one canonical string
Spider trapA URL space that is effectively infinite (calendars, session IDs, faceted search)
Soft 404HTTP 200 with a "not found" body — looks like success, pollutes the corpus
Near-duplicatePages whose content differs only in boilerplate; detected by LSH fingerprints
Freshness / ageFreshness: fraction of pages whose stored copy matches live. Age: time since the live page diverged
Conditional GETRequest with If-None-Match/If-Modified-Since; server returns 304 if unchanged
WARCWeb ARChive format (ISO 28500): raw request/response records in concatenated gzip members

3. The Five Fault Lines#

Each fault line is a place where two competent engineers can disagree. The Staff-level signal is naming the options, the victim of each, and committing to a default with an explicit deviation trigger.

3.1 Fault Line 1: Politeness vs Throughput#

The tension: Every extra request per second to a host makes your index fresher and their server busier. You don't own their capacity, and they don't owe you tolerance.

StrategyWhat WorksWhat BreaksWho Pays
Fixed global delay (e.g., 1 req/s/host)Simple, predictable, rarely complained aboutBig sites (Wikipedia, large retailers) can take 20× more; tiny sites on shared hosting may not take 1/sSearch quality for large sites; small operators on bad days
Adaptive delay (k × response time)Slow servers get space automatically; fast servers get crawled fasterNeeds per-host state; noisy response times cause oscillationCrawl team (tuning); nobody external if bounded
Honor Crawl-delay + 429/503 Retry-AfterRespects explicit operator wishesCrawl-delay: 60 on a 1M-page site means a 2-year crawlFreshness for that host — by the operator's choice
Negotiated higher rates (allowlist)Top hosts crawled at 10–50 req/sRequires relationships and an owner for the listPartnerships/crawl team headcount
Aggressive (ignore signals)Max freshness short-termBans, complaints, reputational and legal damageEveryone — including every other crawl sharing your IPs

Staff default: Adaptive delay with a floor of 1 s and one concurrent connection per registered domain, honoring 429/503 by doubling the delay (capped at 1 hour) and honoring Crawl-delay when present. An allowlist of the top ~1,000 hosts gets higher limits, each entry with a named owner and an expiry.

Per-IP, not just per-host: 10,000 small sites on one shared-hosting IP are one server. Keep a second politeness key on resolved IP (e.g., ≤ 4 concurrent connections per IP) — otherwise host-level politeness still melts the shared box.

When to deviate: Change-monitoring intents with contractual customer SLOs may justify a negotiated faster rate with the site owner — never a unilateral one.

🎯 Staff Move: "Politeness is a hard constraint, not a knob I trade for freshness. If the business needs a host crawled faster than it tolerates, that's a partnership conversation, not a config change."

3.2 Fault Line 2: Freshness vs Coverage#

The tension: With ~40M fetches a day, every recrawl of a known page is a fetch not spent discovering a new one — and vice versa.

StrategyWhat WorksWhat BreaksWho Pays
Uniform recrawl (every page every N days)Easy to reason about, easy to explainMost fetches hit unchanged pages; the news homepage is days staleUsers of fresh content; bandwidth budget
Change-rate proportionalFetches go where changes arePages that change constantly (clocks, tickers) eat the budget without valueTail coverage
Importance × P(changed)Freshness where users lookImportance signal is owned by another team and can be gamedNew/unknown pages (low importance by default)
Fixed split (e.g., 70% recrawl / 30% discovery)Protects discovery from starvationSplit is arbitrary until measuredWhichever side is under-served that week

Staff default: A fixed discovery floor (≥ 20% of daily fetches) so new content is never starved, and the rest allocated by importance × P(changed), with per-tier age SLOs as hard floors. Clamp the per-URL recrawl rate at the tier's minimum interval so hyper-volatile pages don't monopolize the budget.

The math worth saying out loud: With a Poisson change model, if a page changes at rate λ and we recrawl every interval I, its expected freshness is (1 − e^(−λI)) / (λI). A page changing daily (λ = 1/day) recrawled daily is fresh ~63% of the time; recrawled every 6 hours, ~88%. That diminishing return is why "crawl the head more" has a ceiling.

When to deviate: A bulk corpus crawl sets the recrawl share near 0%. A change-monitoring crawl sets discovery to 0%.

3.3 Fault Line 3: Centralized vs Host-Partitioned Frontier#

The tension: A single global priority order is easy to reason about; enforcing politeness across many workers is not.

StrategyWhat WorksWhat BreaksWho Pays
Global queue (Kafka/SQS/Redis ZSET)Simple, easy to scale consumersPoliteness requires a distributed per-host lock on every fetch; hot hosts serialize workersThroughput; the Redis on-call
Global queue + distributed rate limiterReuses a rate limiterWorkers pop URLs they can't fetch yet, re-queue them — churnFrontier CPU and latency
Host-partitioned frontierPoliteness is local and single-writer; no lockRebalancing on worker failure; partitions skew by host sizeFrontier team (rebalancing, skew handling)
Host-partitioned + two-level (domain → subdomain)Handles *.blogspot.com-style hosting correctlyMore complex keying via Public Suffix ListCrawl team (key correctness)

Staff default: Host-partitioned, keyed by registered domain, with a per-IP secondary limit. Partitions (e.g., 4,096) mapped to workers by consistent hashing so rebalancing moves ~1/N of hosts. Frontier state persisted in a local embedded store (RocksDB) with periodic checkpoints to object storage, so a worker restart recovers in minutes rather than re-deriving from the URL-seen store.

Diagram: 3.3 Fault Line 3: Centralized vs Host-Partitioned Frontier

When to deviate: Change monitoring on a small watch-list (< 10M URLs) can use a time-wheel scheduler per host without front queues.

3.4 Fault Line 4: Dedup Precision vs Cost#

The tension: Exact sets cost RAM and disk; probabilistic structures cost silent coverage loss.

LayerOptionCostSilent FailureWho Pays
URL-seenBloom filter only~12 GB for 10B URLs at 1% FPR1% of new URLs never crawled, forever, undetectablySite owners whose pages never appear; search quality
URL-seenExact KV (RocksDB/Cassandra) + Bloom cache~100 GB–1 TB on SSDNone — but lookups must be batchedInfra cost
Exact contentSHA-256 of normalized body32 bytes/pageMisses near-dupsIndex storage
Near-dupSimHash 64-bit, Hamming ≤ 38 bytes/page + permuted tablesFalse positives merge distinct pages (e.g., product variants)Merchants with near-identical SKU pages
Near-dupMinHash + LSH on shinglesHigher CPU, tunable Jaccard thresholdParameter tuning driftCorpus quality team

Staff default: Exact URL-seen store with a Bloom filter as a negative cache in front — if Bloom says "never seen," it's definitely new; if "maybe seen," confirm against the exact store in a batched lookup. SHA-256 for exact content, SimHash for near-dups at the index boundary, with near-dup clusters keeping a canonical representative (respect rel=canonical when it agrees with our cluster).

🎯 Staff Move: "A Bloom filter's false-positive rate is a coverage-loss rate with no alarm attached. I'll use it to skip the disk lookup, never to make the final decision."

3.5 Fault Line 5: Crawl Everything vs Crawl Responsibly#

The tension: The corpus is more valuable the more it includes; the liability grows faster than the value at the edges.

StrategyWhat WorksWhat BreaksWho Pays
Honor robots.txt onlyIndustry baseline (RFC 9309)Doesn't cover AI-use opt-outs, noindex, X-Robots-Tag, takedownsLegal, when a publisher objects to use rather than crawl
Honor all machine-readable opt-outs + takedownsDefensible postureSmaller corpus; needs a policy service and propagationCorpus size; policy team headcount
Fail-open on robots.txt fetch failureKeeps crawling during flaky robotsCrawls paths the operator disallowedThe operator; our reputation
Fail-closed on robots.txt failureNever violates rulesA 5xx on robots.txt stops crawling a siteFreshness for that site

Staff default: Follow RFC 9309 semantics: robots.txt 4xx → treat as "allow all"; 5xx or timeout → treat as "disallow all" temporarily, reuse the last good copy for up to ~24h if we have one, and retry with backoff. Opt-outs and takedowns flow through a policy service with a 24h propagation SLO, enforced at both fetch time and serving/export time — because data already in the corpus must also be purged.

Diagram: 3.5 Fault Line 5: Crawl Everything vs Crawl Responsibly

Who signs off: Legal owns the policy; the crawl team owns enforcement and the propagation SLO; each downstream consumer (index, ML) owns purging its derived copies. Write that down before the first takedown arrives.


4. Failure Modes & Operational Reality#

4.1 Spider Trap — The Infinite Calendar#

t=0:       Host events.example.org added via a new link; budget default 50K/day
t=+10min:  Crawler finds /calendar?month=2026-10, each page links next month
t=+2h:     42,000 URLs discovered on this host; 99% are /calendar?month=...
t=+2h:     crawler.host_new_url_rate{host} = 350/min vs 30-day baseline 0
t=+3h:     Per-host budget exhausted; host drops to lowest band (damage capped)
t=+3h05m:  Trap detector flags pattern: 1 param varying, 97% near-dup ratio
t=+1day:   Pattern rule auto-generated: month > now+24 months → drop
  • Detection: crawler.host_new_url_rate vs baseline, crawler.host_dup_ratio > 0.8, URL-pattern cardinality per path template.
  • Blast radius: Without a per-host budget, one host can consume a partition's entire day — every other host on that partition goes stale. With budget: ~50K wasted fetches, one host.
  • Mitigation: Per-host budget, pattern-based suppression, lowest-priority band for suspects.
  • Prevention: Normalize known-infinite parameters; learn per-host URL templates; cap depth at 16.
  • Owner: Crawl team on-call; trap-rule changes reviewed by the crawl quality owner.

4.2 IP Range Banned by a CDN#

t=0:       A large CDN's bot-management tightens thresholds
t=+15min:  403 rate for hosts behind that CDN rises 0.5% -> 22%
t=+15min:  Bodies are 200/403 challenge pages, ~5KB, identical SimHash
t=+30min:  crawler.http_status_ratio{status=403, asn=cdn} crosses 10% for 15m -> page
t=+45min:  On-call pauses the affected fetch pool; checks reverse-DNS verification
t=+4h:     Contact via CDN's verified-bot program; crawler IPs re-verified
  • Detection: 403/429 ratio by destination ASN; soft-block detection (challenge-page fingerprint cluster).
  • Blast radius: Every host behind that CDN — potentially 10–20% of the web — goes stale.
  • Mitigation: Pause, don't hammer (retrying through a ban deepens it). Serve stale from index.
  • Prevention: Publish crawler IP ranges and reverse-DNS; enroll in CDN verified-bot programs; separate IP pools per crawl purpose so an experimental crawl can't burn the search crawler's reputation.
  • Owner: Crawl team for response; partnerships for CDN relationships.

4.3 DNS Resolver Saturation#

t=0:       New discovery wave: 2M never-seen hosts enter the frontier
t=+5min:   DNS cache hit rate drops 97% -> 60%
t=+8min:   Upstream resolver p99 3s; fetchers block on resolution
t=+10min:  crawler.fetch_rate down 70% although hosts are ready
t=+12min:  Alert on crawler.dns_latency_p99 > 500ms
  • Detection: crawler.dns_cache_hit_ratio, crawler.dns_latency_p99, fetch rate vs ready-host count.
  • Mitigation: Async DNS pre-resolution when a host enters the frontier (not at fetch time); rate-limit new-host admission; run local recursive resolvers per fetch cluster.
  • Owner: Crawl infra.

4.4 Silent Staleness — The Scheduler Stops Scheduling#

The most dangerous failure: nothing errors. A deploy changes the importance-signal join key; 70% of URLs get importance 0; the scheduler dutifully recrawls the tail.

t=0:       Ranking team renames a field in the importance export
t=+1day:   Recrawl scheduler reads null importance -> defaults to 0 for 70% of URLs
t=+1day:   fetch_rate unchanged; error rate unchanged; dashboards green
t=+4days:  Top-tier p90 age: 20h -> 86h
t=+5days:  Search quality team reports stale results for news queries
  • Detection: crawler.freshness_age_p90{tier} against SLO — the metric. Also a canary set: 10K known-volatile URLs whose last-fetch age is alerted directly. Schema checks on the importance feed (null-rate > 1% → reject the feed, keep last good).
  • Blast radius: Whole index freshness, degrading slowly over days.
  • Owner: Crawl team owns the SLO; ranking team owns the feed contract. The contract needs a schema and a consumer-driven test.

🎯 Staff Move: "Fetch rate is a vanity metric. The crawler's SLO is age of the head tier. If I could have one alert, it's freshness_age_p90{tier=top10M} > 24h for 2h."

4.5 Poison Content — The 2 GB "HTML" Page and the Parser Crash Loop#

  • Symptom: Parser workers OOM repeatedly on one partition; the Kafka consumer lag for that partition grows.
  • Root cause: A server streams an endless body, or a gzip bomb (1 MB → 10 GB).
  • Detection: parser.oom_restarts, per-partition consumer lag, fetch.body_bytes_p99.
  • Mitigation: Fetch-side cap (10 MB, truncate and mark), decompression ratio cap (e.g., 100×), parser in a sandbox with memory limit, poison records to a DLQ after 2 failures.
  • Owner: Crawl team; DLQ reviewed weekly.

4.6 Operational Reality Matrix#

FailureDetection SignalBlast RadiusMitigationOwner
Spider trapcrawler.host_new_url_rate, host_dup_ratioOne host's budget (if capped)Budget + pattern suppressionCrawl on-call
CDN/IP ban403/429 ratio by ASN, challenge-page cluster10–20% of webPause, verified-bot programCrawl + partnerships
DNS saturationdns_cache_hit_ratio, dns_latency_p99Whole fetch clusterPre-resolve, local resolversCrawl infra
Silent stalenessfreshness_age_p90{tier}, canary setWhole indexFeed schema checks, last-good fallbackCrawl (SLO) + ranking (feed)
Poison contentparser OOMs, consumer lagOne partitionSize caps, sandbox, DLQCrawl on-call
Frontier disk fullfrontier.disk_used_pct > 80%Partition stops accepting URLsSpill low bands to object storage, tighten admissionCrawl infra
Worker loss + rebalancingfrontier.partition_unowned_seconds~1/N of hosts pauseConsistent hashing, checkpoint restoreCrawl infra
Takedown not propagatedpolicy.takedown_propagation_age > 24hLegal exposureEnforce at export, audit jobPolicy/legal + consumers

5. Evaluation Rubric#

5.1 Level-Based Signals#

DimensionSenior (L5)Staff (L6)Principal (L7)
FramingDesigns a generic crawlerPicks an intent and a freshness target out loudAsks whether the org should own a crawler at all, and for whom
FrontierPriority queue + per-domain rate limitHost-partitioned front/back queues; single-writer politeness; per-IP secondary keyPoliteness contract as org policy; separate IP pools per purpose
DedupBloom filterThree layers with named silent-failure victimsPrices dedup against wasted fetch and index cost
FreshnessFixed recrawl intervalChange-rate estimation, importance × P(changed), discovery floorFreshness SLO per tier negotiated with search leadership, with budget tradeoffs in $
FailureRetries, timeoutsTraps, bans, DNS, silent staleness — each with metric + ownerOrg exposure: complaint SLA, takedown path, correlated risk across crawl consumers
OwnershipOne teamCrawl vs ranking vs legal boundariesCrawl-as-a-platform with quotas and chargeback; decides when not to standardize

5.2 Strong Hire Signals#

SignalWhat It Sounds Like
Structural politeness"Partition by registered domain so one owner enforces the delay — no distributed lock."
Budget thinking"We have ~40M fetches a day. I'll reserve 20% for discovery and spend the rest on importance × P(changed)."
Silent-failure awareness"A Bloom false positive is a page we never crawl and never know we missed."
Replayability"Raw bytes to WARC first. A parser bug becomes a reprocessing job, not a refetch."
Ecosystem ownership"Publish IP ranges and reverse DNS; a verifiable bot gets banned less."

5.3 Lean No-Hire Signals#

SignalWhy It Misses the Bar
"We'll add more fetchers to go faster"Throughput is bounded by host interleaving and politeness, not fetcher count
No mention of robots.txt until askedTreats the ecosystem constraint as an afterthought
Bloom filter as the final dedup authoritySilent, permanent coverage loss with no detection
Uniform recrawlWastes most of the budget; no freshness SLO
"Retry until success" on 429/503Deepens bans; hostile to site operators

5.4 Common False Positives#

  • Detailed HTML parsing knowledge ≠ crawler design. Knowing DOM quirks doesn't show you can allocate a fetch budget.
  • Big storage estimates ≠ scale thinking. "100 TB" is arithmetic; "the top 1,000 hosts cap our head freshness at 86K fetches/host/day" is scale thinking.
  • Naming Kafka for the frontier ≠ a frontier design. A log doesn't enforce per-host timing; the scheduler state is the design.
  • Mentioning SimHash ≠ understanding near-dup. The question is what it costs when it's wrong (merged product variants).

6. Interview Flow & Pivots#

6.1 Typical 45-Minute Shape#

PhaseTimeGoal
Framing + intent0–3 minCommit to search freshness crawl, 1B pages, tiers
Entities + API3–5 minURL record, host record, lease API
Architecture5–10 minLoop diagram; partition by host; raw-bytes-first
Deep dive: frontier10–20 minFront/back queues, politeness math, rebalancing
Deep dive: traps + dedup20–30 minBudgets, normalization, three dedup layers
Deep dive: freshness30–40 minChange-rate model, budget allocation, SLOs
Wrap-up40–45 minOwnership, evolution, what's next

6.2 How Interviewers Pivot — And What They're Testing#

PivotWhat They're TestingStrong Response Direction
"Now crawl 100× more"Do you know the real bottleneck?Hosts × delay, DNS, dedup store, index ingestion — not fetchers
"A site owner complains"Ecosystem ownershipComplaint SLA, per-host override, verified identity
"Pages need JavaScript"Cost judgmentRender farm for a targeted subset; 10–50× CPU per page
"Make it multi-region"Where state livesFrontier stays host-partitioned; fetch from the region nearest the host
"How do you know it's working?"Observability of silent failureFreshness age per tier, canary URLs, coverage sampling

6.3 What to Deliberately Skip#

  • HTML parsing details, character-set detection — "a library, sandboxed."
  • TCP tuning and HTTP/2 multiplexing — mention keep-alive per host batch, move on.
  • Ranking algorithms — importance is an input owned by another team.
  • Exact storage arithmetic beyond one line.

6.4 Follow-Up Questions to Expect#

  1. "How do you handle a site with 100M pages that allows only 1 req/s?" (≈ 3.2 years per full pass — prioritize, use sitemaps, negotiate.)
  2. "How would you detect that a page is a soft 404?"
  3. "What's the URL-seen store's lookup path at 10K new links/s?"
  4. "How do you rebalance the frontier when a worker dies?"
  5. "How would you crawl the same host from two regions without violating politeness?"
  6. "How do you propagate a takedown to data already in the index and in ML datasets?"
  7. "How would you measure coverage — what fraction of the web you're missing?"

7. Active Drills#

Drill 1: The Opening (Intent + Constraints)#

Prompt: "Design a web crawler."

Staff Answer

"Before I size anything — what is the corpus for? A search index needs freshness on important pages; an archive or ML corpus needs coverage and cleanliness with almost no recrawl; change monitoring needs fast detection on a fixed list. Those produce three different schedulers.

I'll assume a search-index crawler: ~1B pages, the top 10M refreshed daily, news hosts hourly, the rest at least every 30 days. That's roughly 40M fetches a day, ~460/s average, peaking ~2× — modest. The hard constraint is politeness: one connection per registered domain, at least 1 s between requests, robots.txt honored. So I'll design the frontier around hosts, not URLs, and spend most of the time on the recrawl scheduler and trap defense."

Why this is L6:

  • Names three intents with different schedulers, commits to one
  • Converts the requirement into a daily fetch budget immediately
  • Declares politeness as the binding constraint before drawing anything

What L7 adds:

  • Asks whether other teams (ML data, trust & safety) already crawl — and whether this should be one platform with shared politeness state
  • Names the legal policy owner before the first opt-out question arrives
❌ Common L5 Trap

"I'll start with seed URLs in a queue, a pool of fetchers that download pages, a parser that extracts links and puts them back on the queue, and a database to store the pages."

Why this misses: It's correct and it's the answer to a different question — "implement BFS." The interviewer's next question, "which pages do you fetch first, and how fast per site?", forces the candidate to retrofit priority and politeness onto a FIFO queue that has neither.


Drill 2: The Throughput Ceiling#

Prompt: "You have 2,000 fetcher machines. How many pages per second can you crawl?"

Staff Answer

"Fetcher count isn't the bound. Three ceilings apply, and I take the minimum:

  1. Politeness: fetches/s ≤ ready_hosts / avg_delay. With 500K active hosts at a 1 s average delay, that's 500K/s — but the distribution matters: if 30% of my high-priority URLs are on 1,000 hosts, those 1,000 hosts cap that slice at ~1,000/s total.
  2. DNS: at < 90% cache hit rate, resolution latency dominates. Pre-resolve on frontier admission.
  3. Bandwidth: at 100 KB/page, 10K pages/s is ~8 Gbps inbound. Fine for a fleet, not for a single NAT gateway.

2,000 machines at ~1K concurrent connections each and ~300 ms fetch latency could do ~6M fetches/s in theory. We'd never get close — politeness and DNS bind first. So I'd run maybe 20–50 fetcher nodes for a 1B-page crawl and put the engineering into host interleaving."

Why this is L6:

  • Identifies the real bound (host interleaving) rather than machine count
  • Uses the politeness formula with concrete numbers
  • Right-sizes the fleet instead of accepting the prompt's premise

What L7 adds:

  • Translates the over-provisioned fleet into dollars and reclaims it
  • Notes that shared NAT/egress is a cross-team correlated bottleneck worth its own capacity plan
❌ Common L5 Trap

"Each machine can handle about 100 fetches per second, so 2,000 machines gives us 200K pages per second."

Why this misses: Linear scaling from fetcher count assumes unlimited, uniformly distributed hosts. The interviewer asks: "Half your queue is on 100 domains. What happens?" Either those domains get 1,000 req/s each (ban) or fetchers idle.


Drill 3: Make the Frontier Concrete#

Prompt: "Walk me through exactly how a URL goes from discovered to fetched."

Staff Answer

"1. Parser extracts https://Shop.Example.com:443/p?id=7&utm_source=x#top. Normalizer yields https://shop.example.com/p?id=7; registered domain example.com. 2. Compute url_hash (64-bit). Batch lookup: Bloom says 'definitely new' → skip the store read; 'maybe' → confirm in the URL-seen store. New → insert with first_seen. 3. Route to frontier partition hash('example.com') mod 4096, owned by worker W17. 4. W17 assigns a priority band from link-source importance and path heuristics, appends to front queue band 3. 5. The selector moves it to example.com's back queue when that queue drains below 20 entries. 6. The heap pops example.com when next_fetch_at ≤ now. W17 checks the robots.txt cache (fetched 6 h ago, TTL 24 h) — allowed. Checks the host budget: 3,100 of 50,000 used today. 7. A fetcher leases a batch of up to 20 example.com URLs, fetches them sequentially over one keep-alive connection, writes each response to WARC, reports response times. 8. W17 sets next_fetch_at = now + max(1s, 10 × avg_response_time)."

Why this is L6:

  • Normalization and registered-domain keying are explicit, not hand-waved
  • The Bloom filter is only a shortcut; the exact store decides
  • Politeness, robots, and budget are checked by the single owner of the host

What L7 adds:

  • The normalizer and Public Suffix List version are shared libraries with a versioning policy — a normalization change re-keys billions of URLs, a one-way door
❌ Common L5 Trap

"The URL gets pushed to Kafka, a fetcher consumes it, checks Redis for the domain's last fetch time, and if it's too soon, puts it back on the queue."

Why this misses: Requeue-on-not-ready creates churn proportional to how hot the host is — the hottest hosts generate the most wasted pops. It also needs a Redis round-trip per fetch for politeness state that a partitioned owner holds locally.


Drill 4: Dedup at 10 Billion URLs#

Prompt: "How do you know you've already seen a URL? And a page?"

Staff Answer

"Three different questions:

URL-seen: exact store keyed by 64-bit hash (collision probability at 10¹⁰ keys is ~0.3% for any collision across the whole set by the birthday bound — acceptable; use 128-bit if you want it negligible). RocksDB on each frontier worker for its own hosts, since URLs are host-partitioned too. A Bloom filter at ~10 bits/key sits in front as a negative cache — it removes ~90% of disk reads because most extracted links are already seen.

Exact content: SHA-256 of the normalized body. Hit → record the alias, don't index twice.

Near-dup: SimHash over shingled visible text; Hamming ≤ 3 on 64 bits is a near-dup. Index with 4 permuted tables keyed on 16-bit blocks so lookups are exact-match probes, not scans. Near-dups form a cluster with one canonical member.

The victims: a URL-seen false positive loses a page forever; a near-dup false positive merges product variants — a merchant's size-M page disappears from search."

Why this is L6:

  • Separates three dedup problems with different tolerances
  • Does birthday-bound math on the hash width
  • Names the victim of each false positive

What L7 adds:

  • Makes near-dup cluster membership a shared asset consumed by index and ML teams, so dedup is computed once, not per consumer
❌ Common L5 Trap

"A Bloom filter holds all seen URLs; for content, hash the page and compare."

Why this misses: Bloom as the authority means silent, permanent coverage loss. An exact content hash misses the dominant duplicate type on the modern web — the same article with a different ad slot or timestamp.


Drill 5: The Spider Trap#

Prompt: "One site generates infinite URLs. What happens, and what do you do?"

Staff Answer

"Without defenses, that host's back queue grows without bound, the partition's disk fills, and every other host on the partition starves. So the design has to cap damage before it detects the cause.

Cap: a per-host daily URL budget (default 50K fetches, 500K frontier entries). A trap can waste at most one host's budget.

Detect: new-URL rate per host versus its 30-day baseline; near-dup ratio within the host > 80%; path-template cardinality (one query param producing > 10K distinct values); repeated path segments; depth > 16.

Respond: suspects drop to the lowest priority band and enter a review queue with a sample of URLs. Automated rules can suppress a parameter pattern; blocking a whole host needs a human, because false positives there lose a real site.

Metric: crawler.trap_suspects_open, plus fraction of daily fetches spent on suspect hosts — if > 2%, page."

Why this is L6:

  • Bounds blast radius first, detects second
  • Multiple independent heuristics, each with a threshold
  • Distinguishes automated pattern suppression from human-reviewed host blocks

What L7 adds:

  • A shared trap-pattern registry across crawl consumers — one team's discovery protects everyone's budget
❌ Common L5 Trap

"Set a maximum crawl depth of 10."

Why this misses: Calendar traps are shallow — /calendar?month=N is depth 1 with infinite breadth. Depth limits also cut off legitimate deep content on large sites.


Drill 6: Freshness Budget#

Prompt: "You can do 40M fetches a day across 1B known pages. How do you decide which?"

Staff Answer

"First, reserve 20% — 8M — for discovery of new URLs, so we never starve coverage.

The other 32M go to recrawl. For each URL I keep an estimated change rate λ from its fetch history (changes observed / time, smoothed; new pages get a prior from their host and path template). The value of recrawling now is importance × P(changed since last fetch) = importance × (1 − e^(−λ·t)).

The scheduler runs per partition every hour: score candidates, take the top N allowed by each host's remaining budget. Hard floors: top-tier URLs never older than 24 h; everything recrawled at least every 90 days to catch deletions.

Conditional GET makes recrawls of unchanged pages cheap in bytes but not in politeness — a 304 is still a request against the host's delay. So the budget is counted in requests, not bytes.

I'd report the result as freshness SLOs: top 10M p90 age < 24 h, news p90 < 2 h, tail p90 < 30 days."

Why this is L6:

  • Explicit discovery floor protects coverage
  • A value function with a defensible model, plus hard floors
  • Notices that 304s save bytes, not politeness budget

What L7 adds:

  • Prices tier SLOs: "Moving the top tier from daily to 6-hourly costs ~30M extra fetches/day — about a doubling of the crawl. Here's the quality delta; you decide."
❌ Common L5 Trap

"Recrawl each page every 25 days so we cover all 1B pages each cycle."

Why this misses: Uniform allocation leaves the homepage of a news site up to 25 days stale while spending fetches on archived pages that haven't changed in years.


Drill 7: robots.txt Is Unreachable#

Prompt: "robots.txt for a major site times out. Do you crawl?"

Staff Answer

"Timeouts and 5xx on robots.txt mean 'unknown', and unknown means don't crawl — RFC 9309 treats server errors as a full disallow. If I have a cached copy under ~24 h old, I use it. Otherwise the host pauses and I retry robots.txt with backoff: 1 min, 5 min, 30 min, capped at hours.

A 404 on robots.txt is different — that means 'no rules', so crawl is allowed.

If robots.txt is unreachable for more than a day on a top-1,000 host, that's a ticket for the crawl on-call, because the site is probably blocking us at the network layer and we need to know why.

Metric: crawler.robots_unknown_hosts{tier}. Owner: crawl team for enforcement, legal for the policy that says fail-closed."

Why this is L6:

  • Distinguishes 4xx (allow) from 5xx/timeout (disallow) with a stated standard
  • Uses last-known-good with an explicit TTL
  • Escalation threshold tied to host importance

What L7 adds:

  • Makes fail-closed on robots a written org policy so an on-call under freshness pressure can't flip it at 3 a.m.
❌ Common L5 Trap

"If we can't get robots.txt, we assume everything is allowed and crawl normally."

Why this misses: A struggling server returning 5xx on robots.txt is exactly the server you shouldn't add load to — and fail-open here means crawling paths the operator disallowed.


Drill 8: Build vs Buy#

Prompt: "The ML team needs 5B clean English pages. Build a crawler?"

Staff Answer

"Start with what exists: Common Crawl publishes billions of pages per crawl as WARC. Filtering, dedup, and quality classification over existing crawls gets most of the corpus without touching a single site. The gaps: freshness, specific domains, and opt-out compliance we can verify ourselves.

So: build the processing pipeline (dedup, quality filters, opt-out enforcement, provenance tracking) — that's where the value is. Top up with a targeted crawl for high-value domains we need fresher. Don't build a general crawler for a one-time corpus.

The cost comparison: a general crawler is 3–5 engineers permanently plus an abuse inbox. A processing pipeline over public dumps is 1–2 engineers and object-storage compute."

Why this is L6:

  • Reuses public data before building
  • Identifies where custom work adds value (processing, compliance)
  • Sizes the ongoing ownership cost

What L7 adds:

  • Flags the licensing/opt-out posture as a one-way door: data trained into a model can't be un-trained cheaply, so provenance must be captured at ingestion
❌ Common L5 Trap

"We'll build our own crawler so we control quality."

Why this misses: Quality is a property of filtering, not fetching. The crawler adds months of work and permanent operational burden without improving the filter.


Drill 9: Changing the Politeness Policy Without an Incident#

Prompt: "Legal wants us to honor a new AI-crawler opt-out directive starting next week."

Staff Answer

"Two parts: enforce going forward, and clean what we already hold.

Forward: add the directive to the robots parser behind a flag. Shadow mode for 3 days: compute what we would have skipped, report the host count and the fetch-volume impact per tier. Canary on 5% of partitions, check freshness SLOs don't regress for unaffected hosts, then enforce.

Backward: a purge job over existing WARC and derived datasets, keyed by host and fetch date. Each consumer (index, ML corpus) confirms purge via a completion record — the policy service tracks propagation and alerts if any consumer exceeds its SLO.

Governance: the policy lives in the crawl policy service, versioned, with legal as approver. The crawler team can't change it unilaterally; legal can't deploy it unilaterally."

Why this is L6:

  • Shadow → canary → enforce rollout with measured impact
  • Handles data at rest, not just new fetches
  • Two-key governance between legal and engineering

What L7 adds:

  • Designs the policy service as a shared control plane for every crawling team, with an audit trail regulators can inspect
❌ Common L5 Trap

"Update the robots.txt parser to recognize the new directive and deploy."

Why this misses: Ignores the data already collected, and gives no visibility into how much of the corpus the change removes before it removes it.


Drill 10: Multi-Region Crawl#

Prompt: "Latency to Asian hosts is 250 ms from us-east. Add a second crawl region?"

Staff Answer

"Yes, but keep one owner per host. Assign each host a home region by measured RTT (or by IP geolocation initially); its frontier partition lives there. Politeness stays single-writer — two regions never fetch the same host concurrently.

What's global: the URL-seen store (or at least a replicated Bloom of it to avoid cross-region lookups for most links), the policy service, and the WARC store (regional buckets, globally cataloged). Discovered links for a host homed elsewhere are forwarded to that region's frontier in batches.

Win: fetch latency 250 ms → ~30 ms for those hosts, so batch throughput per host roughly doubles under the same delay. Cost: cross-region link forwarding and a second fleet — justified once ~20% of the crawl is far from the primary region."

Why this is L6:

  • Preserves single-writer politeness across regions
  • Clear split of global vs regional state
  • Names the trigger that justifies the cost

What L7 adds:

  • Considers data-residency law per region for stored content and chooses bucket placement accordingly
❌ Common L5 Trap

"Run a full crawler in each region, splitting URLs by hash."

Why this misses: Hashing URLs across regions puts the same host in both regions — politeness is violated by 2× with no local way to detect it.


8. Deep Dive Scenarios#

Deep Dive 1: The Election-Night Freshness Incident#

Context: It's election night. Search results for "election results" show pages 3–6 hours stale. The top 200 news hosts update every 2–5 minutes. Search leadership escalates to you at 9 p.m.

Questions to Surface First:

  • Is the crawler fetching these hosts and the indexer lagging, or is the crawler not fetching them?
  • Are these hosts in the news tier with hourly SLOs, or were some mis-tiered?
  • Are we being throttled (429/503) by the news sites themselves under their own traffic spike?
  • What is the freshness SLO for this tier, and who agreed to it?

Typical L5 Approach: Checks the fetch rate (normal), adds fetchers, and bumps priority for the news domains. Fetch rate rises, but the news hosts are already at their politeness ceiling, so the extra capacity goes to the tail. Freshness doesn't move.

Staff Approach: Splits the pipeline: fetch age vs index age. Finds fetches are 10 minutes fresh but indexing of the news partition lags 4 hours — the indexer's queue is flooded by a scheduled full recrawl of a large forum host. Pauses the bulk recrawl, drains the news partition first, and adds a priority lane from crawler to indexer so news never queues behind bulk again.

Principal Approach: Asks why one freshness SLO depended on an unprioritized shared indexer queue, and why "election night" was a surprise. Institutes an event calendar (elections, sports finals, product launches) that pre-shifts recrawl budget and indexing capacity, and makes end-to-end freshness (live change → servable) the SLO owned jointly by crawl and index, with one dashboard and one error budget.

Staff Approach — Full Reasoning
PhaseWhat to Do
Immediate (0–5 min)Compare crawler.fetch_age_p90{tier=news} with index.ingest_lag{tier=news} — which half is stale?
TriageIndexer lag is 4 h; the news partition sits behind 60M bulk records from a scheduled recrawl
Quick fixPause bulk recrawl emission; reprioritize indexer consumption to the news topic; for hosts publishing RSS/sitemaps, poll feeds every 60 s
GuardrailsWatch 429/503 from news hosts — they're under their own load; don't raise per-host rates tonight
Post-mortemSeparate indexer lanes per tier; event calendar; end-to-end freshness SLO

Metrics to Watch: crawler.fetch_age_p90{tier}, index.ingest_lag{tier}, crawler.http_status_ratio{status=429,tier=news}, feed.poll_latency

Organizational Follow-up: Freshness is an end-to-end SLO; crawl and index teams share it. Add an events calendar owned by search product, reviewed weekly.

Ownership Question: "Who decides to pause a scheduled bulk recrawl during an incident?" Staff answer: The crawl on-call, pre-authorized by runbook — bulk recrawl is deferrable by definition, and the runbook says so. No approval chain at 9 p.m.

Key Takeaway: "Fetch rate says nothing about freshness. Measure age end-to-end, and split it at every queue."

What clears the Staff bar:

  • Decomposes staleness into fetch vs index before acting
  • Knows the news hosts are at the politeness ceiling, so more fetchers can't help
  • Fixes the structural cause (shared lane) rather than the symptom

Deep Dive 2: The Silent Coverage Loss#

Context: A quarterly coverage audit samples 100K URLs from a third-party link dataset. 6% of them — on hosts we crawl regularly — are unknown to the crawler. Nobody noticed. You're asked why.

Questions to Surface First:

  • Are the missing URLs unseen (never discovered) or seen-but-never-fetched?
  • Is the loss uniform across hosts, or concentrated on some patterns (query strings, new sections)?
  • When did the URL-seen implementation or the normalizer last change?

Typical L5 Approach: Adds the missing URLs as seeds and increases discovery budget.

Staff Approach: Classifies the missing URLs. 70% were "seen" according to the URL-seen path but have no fetch record — a normalizer change 4 months ago started stripping a query parameter (?page=) that some hosts use for real content, collapsing distinct pages into one canonical URL. The remaining 30% are Bloom false positives from a filter that grew to 3× its design capacity, pushing FPR from 1% to ~15% because nobody resized it. Fixes: revert the normalization rule to a per-host allowlist, rebuild the filter at the right size, and add the exact-store confirmation path the design should always have had.

Principal Approach: Treats coverage as an unmeasured SLO — the real failure. Funds a permanent coverage-sampling pipeline (weekly, against external link datasets and sitemaps), makes normalizer changes a reviewed, versioned, shadow-tested change class, and adds capacity alarms for every probabilistic structure (bloom.fill_ratio > 0.5 → ticket).

Staff Approach — Full Reasoning
PhaseWhat to Do
ImmediateClassify missing URLs: unseen / seen-not-fetched / fetched-but-deduped
TriageDiff normalizer output across versions on the sample; check Bloom fill ratio vs design
Quick fixRe-admit collapsed URLs; resize the filter; confirm Bloom "maybe" results in the exact store
GuardrailsShadow-test normalizer changes: report how many distinct URLs collapse per host before enabling
Post-mortemCoverage SLO; probabilistic-structure capacity alarms

Metrics to Watch: crawler.coverage_sample_hit_ratio, bloom.fill_ratio, bloom.estimated_fpr, normalizer.collapse_ratio{host}

Organizational Follow-up: A coverage owner on the crawl team; quarterly audit becomes weekly automation.

Ownership Question: "Who approves a URL normalization rule change?" Staff answer: Crawl quality owner, after a shadow report shows per-host collapse counts. It's a one-way-ish door: collapsed URLs stop being fetched and slowly vanish from the index.

Key Takeaway: "Every probabilistic or lossy step in a crawler is a place pages disappear silently. Each needs a sampled ground truth."

What clears the Staff bar:

  • Classifies before fixing
  • Treats normalization as a data-loss risk, not a cleanup
  • Adds a ground-truth measurement, not just a fix

Deep Dive 3: Onboarding a Mega-Host#

Context: The company signs a partnership with a marketplace with 400M product pages. They want everything indexed within 30 days and fresh prices daily. Their current allowance is 1 req/s.

Questions to Surface First:

  • 400M pages at 1 req/s = ~12.7 years. What rate will they actually sustain?
  • Do they publish sitemaps with lastmod, or a product feed?
  • Which pages matter — all 400M, or the ~20M with inventory?

Typical L5 Approach: Raises this host's rate limit to 200 req/s so 400M pages crawl in ~23 days.

Staff Approach: Asks for a feed. A daily product-change feed (or sitemaps with accurate lastmod) turns "crawl 400M pages daily" into "fetch the ~5–10M that changed." Negotiates a crawl rate with their SRE team (e.g., 50 req/s off-peak, 10 req/s peak, scheduled windows), registers it in the allowlist with an owner and expiry, and ramps: 10 → 25 → 50 req/s over a week watching their latency. Initial backfill targets in-stock pages first.

Principal Approach: Recognizes this as the first of many partner integrations and designs a "structured ingest" path beside the crawler — feeds and APIs with partner SLAs — so partners stop being special-cased allowlist entries. Prices the choice: feed ingest costs ~1/50th the fetches of a crawl for the same freshness.

Staff Approach — Full Reasoning
PhaseWhat to Do
ScopingSplit 400M into in-stock (~20M), long-tail, discontinued; target in-stock first
NegotiationAgreed rate windows with partner SRE; contact channel for emergencies
Ramp10 → 25 → 50 req/s, gate each step on partner p99 latency and 5xx < 0.5%
FreshnessConsume the change feed; recrawl only changed URLs; weekly verification sample of unchanged
GuardrailAuto-backoff if partner 5xx > 1%; allowlist entry expires in 90 days unless renewed

Metrics to Watch: crawler.host_fetch_rate{host}, crawler.host_latency_p99{host}, feed.lag, crawler.feed_vs_crawl_mismatch_ratio

Organizational Follow-up: Partnerships owns the relationship; crawl owns the rate contract; index owns ingest of feed data.

Ownership Question: "Who gets paged if our crawl takes down the partner's site?" Staff answer: Our crawl on-call — and the partner's contact is in our runbook. The auto-backoff should fire before either of us notices.

Key Takeaway: "The fastest crawl of a large site is the one you replace with a feed."

What clears the Staff bar:

  • Does the 12.7-year math before proposing anything
  • Reaches for feeds/sitemaps over brute force
  • Ramps rate with gates and an expiry

Deep Dive 4: Post-Mortem — We Took Down a Hospital's Website#

Context: A regional hospital's site went down for 40 minutes. Their logs show 60% of requests came from our crawler. It was on shared hosting with 3,000 other sites on one IP. This hits the press. You own the post-mortem.

Questions to Surface First:

  • What was our request rate to that IP (not host) during the window?
  • Did per-host politeness hold? (Probably yes — for each of the 3,000 hosts.)
  • Did we honor 503/429 and response-time backoff?

Typical L5 Approach: Adds the hospital's domain to a blocklist and lowers its rate.

Staff Approach: Identifies the root cause: politeness was keyed only on host. A discovery wave admitted 400 of the 3,000 co-hosted sites at once; each was individually polite (1 req/s), so the shared IP took ~400 req/s. Fix: add a per-IP concurrency and rate limit (e.g., ≤ 4 concurrent, ≤ 5 req/s per IP), resolved at admission time; make response-time backoff apply at the IP level; add an alert on per-IP request rate.

Principal Approach: Treats it as a policy failure: the politeness standard never defined the unit of "a server." Updates the org's crawl standard to require host and IP limits for every crawler in the company, adds a public complaint channel with a 1-hour response SLA, and runs a quarterly game day simulating shared-hosting load.

Staff Approach — Full Reasoning
PhaseWhat to Do
ImmediateStop all fetches to that IP; confirm the site recovers; contact the hospital
TriageReconstruct per-IP rate from fetch logs; confirm each host was individually polite
Quick fixPer-IP limit deployed behind a flag, enabled globally within 24h
GuardrailsAlert: crawler.ip_request_rate > 5/s for 5 min
Post-mortemPoliteness standard defines the server unit; complaint SLA; game day
t=0:        Discovery wave admits 400 hosts on one shared IP
t=+2min:    Per-IP rate 400 req/s; server p99 800ms -> 9s
t=+6min:    Server returns 503; host-level backoff kicks in per host, slowly
t=+10min:   Site down; our per-host dashboards show nothing abnormal
t=+40min:   Hospital's host provider blocks our range; site recovers
t=+2h:      Complaint reaches us via cloud provider abuse desk

Metrics to Watch: crawler.ip_request_rate, crawler.ip_5xx_ratio, abuse.complaints_open, abuse.complaint_response_time

Organizational Follow-up: Public-facing complaint channel with on-call routing; comms owns external response.

Ownership Question: "Who talks to the press?" Staff answer: Comms, with the post-mortem facts from us within 24 hours. Engineering's job is a timeline and a shipped fix, not a statement.

Key Takeaway: "Politeness has to be keyed on the thing that falls over — the server — not the name you used to reach it."

What clears the Staff bar:

  • Finds the keying flaw, not a one-off blocklist
  • Separates response, fix, and standard change
  • Treats the ecosystem relationship as part of the incident

Deep Dive 5: Multi-Region Expansion and Data Residency#

Context: The company launches search in the EU and Asia. Local results quality is poor: regional sites are crawled from us-east with 200–300 ms RTTs, some geo-block US IPs, and legal flags that EU-origin content must be stored in-region.

Questions to Surface First:

  • What fraction of target hosts geo-block or serve different content to US IPs?
  • Does residency apply to raw fetched bytes, derived index, or both?
  • Does global dedup require cross-region content comparison?

Typical L5 Approach: Deploys a complete crawler copy in each region with its own URL sets.

Staff Approach: Assigns each host a home region (geo-IP + measured RTT + geo-block detection). Frontier partitions and WARC buckets live in the home region, so politeness stays single-writer and bytes stay in-region. Global state is small and metadata-only: URL-seen hashes, SimHash fingerprints, host assignments. Cross-region dedup compares fingerprints, never content.

Principal Approach: Decides residency is a platform capability, not a crawler feature: a data-classification tag on every stored object, enforced by storage policy for every downstream consumer. Weighs the one-way door: once derived datasets mix regions, separating them later means rebuilding from raw — so the tag must exist from day one.

Staff Approach — Full Reasoning
PhaseWhat to Do
AssignmentHost → home region by RTT and geo-block probing; reassess monthly
State splitRegional: frontier, WARC, parsed text. Global: URL hashes, fingerprints, policy
Link forwardingBatched cross-region forwarding of discovered links to the home region
ResidencyRegion tag on every WARC record; export jobs check tags
RolloutMove 5% of EU hosts, compare freshness and 403 rates, then migrate the rest

Metrics to Watch: crawler.fetch_latency_p50{region}, crawler.geo_block_rate{region}, crawler.cross_region_forward_lag, residency.violations

Organizational Follow-up: Legal defines residency classes; platform storage enforces; crawl tags at write.

Ownership Question: "Who owns reassigning a host between regions?" Staff answer: The crawl team's assignment service, automatically, with a drain-then-move protocol so no two regions own a host concurrently.

Key Takeaway: "Regionalize bytes and politeness; globalize only fingerprints and policy."

What clears the Staff bar:

  • Keeps single-writer politeness across regions
  • Moves metadata, not content, across borders
  • Treats residency as a storage tag enforced downstream

9. Level Expectations Summary#

After studying this case study, you should be able to:

  • Name the three crawler intents and explain why each needs a different scheduler
  • Derive the throughput ceiling from host count and politeness delay, not fetcher count
  • Draw a host-partitioned frontier with front/back queues and explain why politeness needs no lock
  • Separate URL-seen, exact-content, and near-dup deduplication, and name the victim of each false positive
  • Allocate a daily fetch budget with a discovery floor and importance × P(changed)
  • Defend against spider traps with budgets first and heuristics second
  • State RFC 9309 robots.txt failure semantics and who owns the policy
  • Name the one freshness metric you'd page on
  • Price the crawler at three scales and name its one-way doors

The Bar for This Question#

Mid-level (L4): Builds the fetch-parse-enqueue loop, handles retries, stores pages. Mentions robots.txt when prompted. Treats dedup as a hash set.

Senior (L5): Adds a distributed priority queue, per-domain rate limiting, a Bloom filter, and a recrawl interval. Sizes storage correctly. The design works but spends budget uniformly, keys politeness wrongly for shared hosting, and has no freshness SLO — it fails quietly.

Staff+ (L6): Commits to an intent, frames the design around a fetch budget, makes politeness structural via host partitioning, layers dedup with explicit silent-failure owners, defends traps with per-host budgets, and measures freshness end-to-end with named metrics and owners. Talks about site operators as stakeholders. The interviewer should learn something from the answer.


10. Staff Insiders: Controversial Opinions#

10.1 "The Crawler's Throughput Number Is Meaningless"#

EvidenceImplication
Fetches/s can rise while freshness falls (Deep Dive 1)Rate measures effort, not outcome
Recrawls of unchanged pages are most of the traffic under uniform schedulesHigh throughput can be mostly waste
Politeness caps the head of the web at ~86K fetches/host/dayAdding throughput mostly adds tail fetches

The Staff position: Report freshness age per tier and coverage ratio. Throughput is a capacity metric for the infra team, not a success metric.

Why this matters in interviews: Candidates who optimize pages/s signal they haven't owned a crawler's outcome.

10.2 "Bloom Filters Don't Belong on the Decision Path"#

EvidenceImplication
FPR grows superlinearly past design capacitySilent loss accelerates as the crawl succeeds
False positives are never re-checkedLoss is permanent without an exact confirmation
An exact 10B-key store on SSD is ~100s of GBThe cost argument for Bloom-only is weak today

The Staff position: Bloom as a negative cache in front of an exact store. Never the authority.

Why this matters in interviews: It shows you reason about failure visibility, not just memory efficiency.

10.3 "Most Companies Should Never Build a General Crawler"#

EvidenceImplication
Public crawls cover billions of pages per snapshotCoverage is a commodity
A general crawler needs an abuse inbox, legal policy, and permanent on-callTCO dominated by people, not machines
Most needs are targeted (partners, known lists)Feeds, APIs, and targeted fetchers suffice

The Staff position: Build the processing pipeline and targeted fetchers; buy or reuse coverage.

Why this matters in interviews: When asked "build vs buy," the strong answer questions the premise.

10.4 "Politeness Is a Product Feature"#

EvidenceImplication
CDNs run verified-bot programsReputation determines access
A banned range loses 10–20% of the web at oncePoliteness is availability for your data source
Complaints escalate to cloud providers and the pressThe blast radius is the company's brand

The Staff position: Budget engineering time for crawler identity, complaint handling, and per-IP limits as first-class features.

Why this matters in interviews: It reframes an "ethics footnote" as an availability requirement.

10.5 "Raw Bytes Are the Only Durable Asset"#

EvidenceImplication
Parsers, extractors, and quality models change monthlyParsed output ages fast
Refetching 1B pages costs a month of politeness budgetRefetch is not a recovery plan
Content changes — the old copy is gone from the webRaw history is irreplaceable

The Staff position: Write WARC first, parse downstream, keep raw for a defined retention owned by legal.

Why this matters in interviews: It turns "what if the parser has a bug?" into a one-sentence answer.


11. The Principal Lens (L7)#

Why L7 Sees This Problem Differently#

A Staff engineer designs a crawler. A Principal engineer notices that search, ML data, trust & safety, ads quality, and a price-intelligence team each run their own crawler — five bot identities, five politeness implementations, five legal postures, all hitting the same hosts from the same cloud IP ranges. The real problem is that the organization's relationship with the open web is fragmented: one team's aggressive experiment gets the whole company's IP range banned, and a takedown request has to be honored by five systems nobody inventoried. The L7 move is to decide what is centralized (identity, politeness, policy, raw storage) and what stays per-team (schedulers, parsers).

The Org-Level Fault Line#

One crawl platform vs per-team crawlers.

OptionWhat WorksWhat BreaksWho Pays
Per-team crawlersAutonomy, fast iteration per use caseDuplicate fetches to the same hosts; inconsistent politeness; shared IP reputation; N takedown pathsSite operators; legal; every team when one gets banned
One platform, shared schedulerSingle politeness state, one identity, one policyPlatform becomes a bottleneck; search priorities dominateSmaller consumers (their freshness needs lose)
Shared fetch + policy layer, per-consumer schedulersPoliteness and compliance central; scheduling autonomousNeeds quota and priority arbitration across consumersPlatform team (arbitration), consumers (quotas)

The L7 default: Centralize the fetch layer, politeness state, bot identity, policy service, and raw WARC store. Consumers submit fetch requests with priorities under a per-consumer quota; each keeps its own scheduler. Chargeback per million fetches makes the cost visible.

🧭 Principal Move: "I'd centralize everything that touches someone else's server — identity, politeness, policy — and decentralize everything that expresses a team's product judgment, like scheduling and parsing."

Cost Model#

Assumptions: cloud list prices, ~100 KB average raw page, ~25 KB compressed, 3-month raw retention, fully loaded engineer cost ~$25K/month, inbound bandwidth free, public-IP fetchers (no managed NAT per-GB charges).

ScaleFetches / monthInfra $/monthHeadcountOn-call Load
Small — 1B pages, monthly cycle~1.2B~$20–30K (fetch ~$3K, frontier SSD ~$8K, storage ~$2K, processing/dedup ~$8K)3–4 engineers (~$90K)1 shared rotation, ~1–2 pages/week
Medium — 10B pages, tiered freshness~12B~$200–300K8–12 engineers + 1 policy/partner ownerDedicated rotation, ~3–5 pages/week
Large — 50B+ pages with JS rendering for 5%~60B+~$1.5–4M (render farm is 50–70%)25–40 engineers across fetch, render, scheduling, policyMultiple rotations; abuse desk staffed

The number that surprises executives: at small scale, people cost 3–4× the infrastructure. At large scale, JavaScript rendering alone can exceed the rest of the crawler combined. The most expensive decision at every scale is "render everything."

The 3-Year Evolution Path#

Diagram: The 3-Year Evolution Path

One-Way Doors vs Two-Way Doors#

DecisionDoorReversal Cost
URL normalization rules and canonical hashingOne-way-ishRe-keying 10¹⁰ URLs; collapsed URLs silently stop being fetched
Raw WARC retention (or not)One-wayDiscarded bytes are unrecoverable; the web has changed
Bot identity / User-Agent and IP rangesOne-way-ishReputation and allowlists are tied to them; changing resets trust
Training data mixed without provenanceOne-wayCan't un-train a model; must rebuild corpus
Frontier partition countTwo-wayRebalance with consistent hashing; hours of work
Recrawl scoring functionTwo-wayShadow and swap; freshness recovers within a cycle
Fetcher language/runtimeTwo-wayStateless fleet; replace gradually

The Standard I'd Write#

RFC: Web Crawling Standard v1

Scope: Any system in the company that issues automated HTTP requests to hosts we don't own.

MUST:

  • Use the shared fetch platform or a registered bot identity with a documented User-Agent, contact URL, and reverse-DNS-verifiable IPs.
  • Enforce politeness per registered domain and per resolved IP: ≤ 1 concurrent connection per domain, ≤ 4 per IP, ≥ 1 s spacing by default.
  • Honor robots.txt per RFC 9309 (5xx/timeout = disallow; cached copy ≤ 24 h), 429/503 Retry-After, and registered opt-out directives.
  • Propagate takedowns to all stored and derived data within 24 h and report completion to the policy service.
  • Store provenance (URL, fetch time, robots state) with every stored document.

SHOULD:

  • Prefer feeds, sitemaps, and APIs over crawling when available.
  • Use conditional GET for all recrawls.

Exceptions: Filed with the crawl platform team; approved by crawl platform lead + legal; expire after 90 days.

Success metrics: Abuse complaints per billion fetches < 1; takedown propagation p99 < 24 h; zero unregistered crawlers found in quarterly egress audits.

What I'd Tell the VP#

We crawl the web for five different products, and today each team does it separately — which means five ways to get our IP addresses banned and five places a legal takedown can be missed. I'm proposing one shared crawl platform that owns our identity, our manners toward other sites, and our compliance, while each product keeps control of what it wants to crawl. It costs roughly two additional engineers for a year and reduces duplicate fetching by about a third. The biggest risk we remove is a single team's mistake taking every product's data supply offline. The biggest cost we avoid is rendering JavaScript for everything; we'll render only the ~5% of pages where it changes results.

Principal Interview Signals#

SignalWhat It Sounds Like
Org-scope framing"How many teams here already send automated requests to the web? Let's start from that inventory."
Pricing tradeoffs"Daily to 6-hourly for the top tier roughly doubles fetch cost; here's the quality delta."
One-way door awareness"Normalization rules and raw retention are the decisions I'd review hardest — we can't undo them."
Standards authorship"I'd write a crawl standard with MUST-level politeness and a registered-identity requirement, and audit egress quarterly."
Correlated risk"All our crawlers share cloud IP reputation; one team's ban is everyone's outage."

Staff answers that L7 interviewers find insufficient:

  • "We'll build a great crawler for search" — correct, but ignores the four other teams crawling the same hosts.
  • "Legal owns robots policy" — names an owner but doesn't design the propagation path or audit.
  • "Freshness SLO p90 < 24 h" — a good SLO with no price attached; L7 says what it costs and what the next tier would cost.

Appendices

Appendix A: Mechanics in Depth#

A.1 URL Normalization#

normalize(url):
  u = parse(url)
  u.scheme = lower(u.scheme); u.host = lower(idna_encode(u.host))
  drop default port (80 for http, 443 for https)
  u.path = remove_dot_segments(u.path); percent-decode unreserved chars
  drop fragment
  params = [p for p in u.query if p.name not in SESSION_PARAMS
                                and not p.name.startswith("utm_")]
  params = sort(params) unless host in ORDER_SENSITIVE_HOSTS
  return serialize(u)

Why it's dangerous: every rule is a potential collapse of distinct pages. Rules are versioned; per-host exceptions exist; changes are shadow-tested with a collapse report (Deep Dive 2).

A.2 Adaptive Politeness#

on_fetch_complete(host, response_time, status):
  if status in (429, 503):
      host.delay = min(host.delay * 2, 3600s)
      if retry_after present: host.delay = max(host.delay, retry_after)
  else:
      target = max(host.min_delay, 10 * response_time)
      host.delay = 0.8 * host.delay + 0.2 * target   # smoothed
  host.next_fetch_at = now + host.delay
  ip.next_fetch_at   = max(ip.next_fetch_at, now + ip.min_delay)

A.3 URL Lifecycle#

Diagram: A.3 URL Lifecycle

A.4 Change-Rate Estimation#

A simple, robust estimator: over the last n fetches at intervals Iᵢ, with X changes observed, λ ≈ −ln(1 − X/n) / mean(I) (valid when X < n). Seed new URLs with a prior from their host and path template. Clamp λ to the tier's min and max recrawl interval.

A.5 SimHash#

simhash(tokens):
  v = [0] * 64
  for t, weight in shingle_weights(tokens):
      h = hash64(t)
      for i in 0..63: v[i] += weight if bit(h, i) else -weight
  return bits(v[i] > 0 for i in 0..63)
near_dup(a, b) = popcount(a XOR b) <= 3

Index: split 64 bits into 4 blocks of 16; store 4 tables each keyed by one block. Any pair within distance 3 matches exactly in at least one block (pigeonhole), so lookups are 4 exact probes plus candidate verification.

Appendix B: Keys and Data Model#

KeyConstructionNotes
url_hash64-bit hash of normalized URL~0.3% chance of any collision at 10¹⁰ (birthday); use 128-bit to make it negligible
host_keyregistered domain via Public Suffix ListPartitioning key for frontier and politeness
ip_keyresolved IP (or /24 for large pools)Secondary politeness key
content_hashSHA-256 of normalized bodyExact dedup
simhash64-bitNear-dup cluster membership
WARC record IDurn:uuid + (file, offset) pointerStored in UrlRecord.body_ref

Appendix C: Coordination Mechanisms#

MechanismUsed ForWhy
Consistent hashing of partitions → workersFrontier ownershipMoves ~1/N hosts on membership change
Lease with fencing tokenPartition ownership handoffPrevents two owners fetching one host after a pause
ZooKeeper/etcd membershipWorker livenessSee ZooKeeper & etcd
Kafka topics by partitionFetch results, discovered linksReplayable, ordered per partition; see Kafka
Checkpoint to object storageFrontier recoveryRestart in minutes, not hours

Quick comparison: a distributed lock per host (Redis) works to ~10K fetches/s and becomes the hot path; partition ownership scales linearly with no per-fetch coordination. Use locks only for rare administrative operations. See Distributed Coordination.

Appendix D: Client (Crawler) Behavior Contract#

BehaviorContract
User-AgentExampleBot/2.1 (+https://example.com/bot) — stable, documented
VerificationReverse DNS of crawler IPs resolves to *.crawl.example.com, forward-confirms
Conditional requestsAlways send If-None-Match / If-Modified-Since on recrawl
CompressionAccept-Encoding: gzip, br; decompression ratio cap 100×
Limits30 s total timeout, 10 MB body cap, max 5 redirects
Backoff429/503 honored with Retry-After; exponential otherwise, capped at 1 h
RetriesMax 3 per URL per cycle, with jitter; never retry 4xx except 408/429

Appendix E: Observability#

E.1 Core Metrics#

Outcome:     crawler.freshness_age_p90{tier}, crawler.coverage_sample_hit_ratio
Budget:      crawler.fetches_by_reason{discovery|recrawl|feed}, crawler.budget_denied{tier}
Politeness:  crawler.host_delay_p50, crawler.ip_request_rate_max, crawler.http_status_ratio{status}
Health:      crawler.dns_cache_hit_ratio, crawler.fetch_latency_p99, frontier.disk_used_pct
Quality:     crawler.dup_ratio{exact|near}, crawler.trap_suspects_open, crawler.soft404_ratio
Ecosystem:   abuse.complaints_open, policy.takedown_propagation_age_p99

E.2 Critical Alerts#

AlertThresholdAction
Head freshnessfreshness_age_p90{tier=top} > 24h for 2hPage
Ban wave403/429 ratio by ASN > 10% for 15 minPage
Per-IP overloadip_request_rate > 5/s for 5 minPage
Trap budgetsuspect-host fetch share > 2%Ticket
Takedown SLOpropagation age > 24hPage policy owner
Bloom capacityfill ratio > 0.5Ticket

E.3 Control Plane vs Data Plane#

Control plane: policy service, recrawl scheduler, host assignment, allowlists — changes rolled out shadow → canary → enforce. Data plane: frontier partitions, fetchers, parsers — must keep running with last-known-good policy if the control plane is down (fail-static), except that robots/takedown enforcement never fails open.

E.4 Debugging Silent Staleness#

  1. Split age by stage: live change → fetched → parsed → indexed.
  2. Check the scheduler's inputs: importance-feed null rate, λ distribution shift.
  3. Check budget denial by tier — is the top tier being denied?
  4. Check canary URLs (10K known-volatile pages) directly.

Appendix F: Scale Evolution#

F.1 What Works at Each Scale#

ScaleArchitecture
< 10M pagesSingle process, async fetcher, SQLite/Postgres frontier, per-host dict
10M–1BHost-partitioned frontier on 10–50 workers, RocksDB, WARC in object storage
1B–50BTiered scheduler, shared fetch platform, per-IP politeness, multi-region home hosts
50B+Render farm, feed ingest, per-consumer quotas, chargeback

F.2 Multi-Region Path#

Start with one region. Add a second when > 20% of target hosts are > 150 ms away or geo-block. Assign hosts home regions; forward links; keep global only hashes, fingerprints, and policy (Drill 10, Deep Dive 5).

F.3 What You Don't Build on Day One#

  • JavaScript rendering (target 1–5% later)
  • Multi-region fetch
  • Learned change-rate models (start with the Poisson estimator)
  • Near-dup across all history (start with within-host)
  • A general platform API (until a second consumer exists)

Appendix G: Multi-Tenancy, Fairness, and Cost#

When the fetch layer serves multiple consumers:

ConcernMechanism
Politeness is sharedOne per-host/per-IP budget across all consumers — the site sees one crawler
Priority across consumersWeighted fair queuing per host: search 60%, ML 25%, others 15% by default
Dedup across consumersA fetch for one consumer satisfies others within a freshness window (e.g., 6 h)
QuotaPer-consumer monthly fetch quota; overage requires approval
Chargeback$ per million fetches + $ per TB stored; published monthly
IsolationExperimental consumers use a separate IP pool and lower per-host share

🧭 Principal Insight: "The site operator should never be able to tell how many teams we have. One identity, one budget, one set of manners."

Related reading: Distributed Rate Limiting for per-key budgets, Message Queues for the fetch-result pipeline, Search Indexing for the downstream consumer, Task Scheduling for time-based scheduling, Consistent Hashing, and Data Pipelines.

  1. Loading the index…