System designby Learnastra

System-design interview · Core interviews

Design a web crawler

By Anup Rai

Design a durable URL frontier, polite per-origin dispatch and replayable fetch/parse stages; control deduplication, unbounded discovery and stale-worker recovery.

You will learn to

  • Separate discovery, fetching, parsing, and storage responsibilities.
  • Demonstrate why URL deduplication differs from content deduplication.
  • Recover unfinished work while preserving per-site fetch constraints.

Practice in this chapter

8 interview questions with model answers and follow-ups.

Go to interview practice

Useful foundations: Message queues, event logs, delivery guarantees, and backpressure · Database indexes: B-trees, composite keys and query access · Data partitioning and sharding · Production readiness: SLI, SLO, observability, and recovery

Workload and timing examples are interview assumptions.

01Problem and scope

A web crawler discovers and retrieves eligible resources by following links from seed URLs. Its engineering responsibilities are durable scheduling, bounded network access, per-origin politeness and recoverable processing of fetched bytes. An origin is a scheme, hostname and port; per-origin politeness limits how frequently and concurrently the crawler requests that site. For example, fetching https://example.org/articles/1 can discover two more URLs that must enter the durable frontier: the stored collection of URLs waiting to be fetched or revisited. A search corpus retains bodies for indexing and reprocessing; link validation, monitoring, mirroring and specialized-media crawls may require different retention and revisit policies.

Begin with one worker and a list of unvisited URLs. Breadth-first traversal uses a first-in-first-out queue to spread discovery; depth-first traversal follows one branch and may reuse a connection. Neither guarantees useful coverage when a site generates endless addresses.

Scope the exercise to a public engineering-article search corpus refreshed on a schedule, subject to site policy and explicit crawl budgets. The crawler must not ignore those restrictions to meet a throughput target. An unbounded, changing web has no reliable global “finished” state; define progress over eligible discovered URLs and freshness targets for prioritized resources.

A parse manifest is a durable record of a parser’s output, including the links it discovered. Keeping that output lets the crawler resume adding links to the frontier after a crash, instead of having to infer whether a downloaded page was fully processed.

Three components protect different work: the frontier remembers unfinished URLs, the egress gate controls requests to each origin, and saved parse manifests preserve discovered links. The bounded example uses U17 discovering U18 and U19 to test those boundaries. Two workers must not overload one origin, and a crash or retry must not silently lose discovery or create uncontrolled duplicate work.

02Functional requirements

  1. Accept seeds and discover links: Add seed URLs, follow eligible links, prioritize important/change-prone pages, and assign revisit budgets. Path-ascending discovery may inspect /articles/ and / under the same checks.
  2. Enforce crawl policy: Fetch and cache /robots.txt for the crawler's user agent under the Robots Exclusion Protocol. The filename is plural; its rules express crawl policy, not access authorization. RFC 9309.
  3. Schedule fetches and revisits: Keep “already seen” distinct from “never fetch again.” Retrieve bounded responses and persist bodies and metadata.
  4. Process and export content: Parse supported content and export eligible documents to the search pipeline. Completed URLs record response status, fetch time, content digest and processing generation.
  5. Operate a recoverable crawl: Let operators pause a site, inspect failed URLs, change budgets and resume work.

Outcomes that control scheduling

Outcome Required action
Redirect Recheck scope, destination safety and policy at the new target; do not inherit trust from the original URL.
Robots disallow Record a skipped outcome, not an aggressively retried network error.
Transient HTTP failure Schedule delayed retry.
Repeated/pathological failure Enter an inspectable terminal or quarantine state.

Scope and content handlers

Assume HTTP/HTTPS HTML and 15B discovered eligible pages over four weeks. MIME type identifies the downloaded content kind; handlers remain modular for future protocols/types. Authenticated content, evading restrictions and a literally complete crawl of an infinite changing web are non-goals.

HTML link extraction is the first processor. Later image/video handlers can reuse bytes and metadata without parsing binary content as markup. A link validator might discard bodies after processing; this search corpus retains them for replay and extraction fixes.

03Non-functional requirements

  1. Workload: Fetch 15 billion eligible pages over four weeks; assume 100 KB average responses and one-second normal network service time.
  2. Availability: 99.9% scheduler availability. A specific site may stop indefinitely when its policy or health requires it.
  3. Politeness: Configure per-origin concurrency and spacing. For the worked origin, permit one active fetch and at least two seconds between starts; too few independent origins can limit global throughput.
  4. Durable scheduling: Persist accepted frontier items and lease transitions before reporting success. A worker crash may repeat a fetch, but must not permanently lose a URL or duplicate document publication.
  5. Replayable extraction: Store raw content durably before marking extraction complete so parser fixes can reprocess it.
  6. Recrawl freshness: Define site classes—for example, important articles within one day and low-value pages within a month. One global average can hide neglected sites.
  7. Resource bounds: Limit URL length, redirects, compressed/uncompressed body size, fetch time, parser CPU and per-site discovery. A page expanding to gigabytes cannot consume an unlimited worker.
  8. Destination safety: Enforce destination restrictions even when doing so slows the crawl.

Processing guarantee

Network uncertainty prevents exactly-once HTTP retrieval. The promised invariant is durable, idempotent processing of accepted work under a polite dispatch policy; retries and content publication must be designed around that boundary.

04Capacity estimates

Worked estimates

Quantity Worked calculation Consequence
Fetch rate 15B / (28 × 86,400 s) ≈ 6,200 pages/s Many hosts and workers
Download payload 6,200/s × 100 KB ≈ 620 MB/s Network and storage throughput matter
Raw storage 15B × (100 KB + 500 B) = 1.5075 PB Persistent object storage
At 70% occupancy 1.5075 / 0.7 ≈ 2.15 PB Before replicas
Concurrent fetches 6,200/s × 1-second average ≈ 6,200 Async I/O or enough bounded workers

Capacity implications and limits

Hundreds of millions of frontier URLs cannot remain in one queue in RAM. Use persistent queues with separate buffered enqueue/dequeue batches. Cache Domain Name System (DNS) address lookups for their allowed time to live (TTL); repeated lookups otherwise waste part of the fetch budget.

With one fetch start every two seconds per origin, 6,200 fetches/s needs at least 12,400 continuously eligible independent origins. If the corpus concentrates on 1,000 such origins, the polite ceiling is roughly 500 starts/s and the four-week target must change. More machines cannot manufacture permission to fetch a site faster.

Assume an average page yields ten candidate links. The URL gate sees about 62,000 candidate links/s before deduplication, often far more than successful new pages. At 200 bytes per retained frontier record, a billion pending URLs is 200 GB before indexes and replication; keep only bounded ready batches in memory. At 620 MB/s, one day of raw payload is about 53.6 TB. Three replicas of the 1.5075 PB logical corpus would exceed 4.5 PB before slack; an erasure-coded cold store can reduce that multiplier with different repair costs.

Little's Law estimates 6,200 in-flight requests at one second, but a ten-second timeout tail can consume many more slots. Use independent global and per-origin limits, and measure connection occupancy rather than assuming worker count equals useful throughput.

05APIs and contracts

Request and response example

These are internal calls between discovery workers, the scheduler and processing workers. A work lease temporarily gives one worker authority to update a particular fetch attempt; its token identifies that attempt. Permission to send network traffic remains subject to the separate origin-policy checks.

enqueue(url, sourceUrl, priority, crawlGeneration)
  → {urlId:U17, state:pending|already_known|rejected}
leaseNext(workerId, capacity)
  → {urlId:U17, attempt:A9, leaseToken:L9, expiresAt, origin}
complete(L9, bodyRef:P84, digest:H4, status:200, parseManifest:M3)
  → {accepted:true, currentState:complete}

The scheduler assigns lease tokens; workers cannot invent completion authority. Every mutation supplies the token and expected attempt. A stale token returns a harmless stale-attempt result and cannot replace a newer completed fetch. Discovery events carry their parent fetch/parse generation, allowing extraction replay without losing provenance. Large extracted link sets go in a durable manifest rather than one unbounded remote procedure call (RPC) between services.

The administrative API supports per-origin pause, priority and revisit policies with audited versions. Fetch results preserve status, redirect chain, headers needed for conditional retrieval, timing and a bounded error category. A 304 response can reference the prior body when the conditional request contract is valid; it is not an empty replacement document.

Public target websites do not receive our internal lease IDs as trust signals. The HTTP fetcher identifies its crawler user agent and uses the applicable site policies. A failed completion RPC is retried with L9 and the same stored body/manifest; it does not require downloading the page again while the lease remains valid.

06Data model and access patterns

The scheduler must remember both URL progress and origin-wide limits. The first three rows show state used to accept and recover URL work; the host schedule coordinates all requests to the same origin, including requests for different URLs.

Operation Example state
enqueue(url, source, priority) U17, parent=seed, state=pending
leaseNext(worker) U17, owner=w3, token=L9, leaseUntil=12:00:30
complete(token, result) L9 → body=P84, hash=H4, status=200
Host schedule example.org, nextAllowed=12:00:02, active=1

Preserve canonical URL strings, discovery source, retry count, and next-due time. A hash can accelerate membership checks but cannot reconstruct the URL to fetch. Canonicalization removes fragments and normalizes safe equivalents; dropping every query parameter can incorrectly merge distinct pages. Domain, prefix, and protocol filters enforce crawl scope before enqueueing.

Maintain exact URL membership in a partitioned store, including canonical string, URL ID, current crawl generation, due time, state and attempt. The frontier indexes due work by priority/time within origin ownership. HostSchedule stores next-start time, active requests and policy version. The raw object store keeps immutable body objects; a content-digest index links identical bytes to one processing representation where safe.

Separate fetch metadata from content identity: two addresses can serve identical bytes but have different robots rules, canonical links, timestamps or crawl provenance. Do not erase those URL records merely because body deduplication succeeds. A parser's output manifest records resolved discovered links and extraction version, so downstream publication can be replayed.

Partition scheduling by origin/host responsibility to coordinate politeness. Partition global digest checks by digest to find mirrors across unrelated hosts. This means URL scheduling and content deduplication have different keys and may live on different owners; there is no assumed cross-store transaction. Save each stage’s result, then let the next stage retry from that record without duplicating its effects.

The URL-membership insert and a durable enqueue intention must commit together at the URL authority. If membership and frontier use different stores, insert the URL plus an outbox event in one local transaction, then relay an idempotent (urlId,crawlGeneration) job. A crash after “already known” but before a separate queue send must not strand the URL forever. A repeated discovery returns the existing record and leaves its pending enqueue intention recoverable. The frontier may instead be an index over that same authoritative URL table, avoiding the extra relay in the baseline.

07Basic working design

One durable scheduler and body store

The baseline has one scheduler/worker process, a local durable frontier database and a body directory/object store. A queue chooses an eligible URL, records a lease, checks robots and per-origin time, fetches under limits, stores bytes, parses them and records a durable link manifest. It then marks the attempt complete and feeds manifest entries back to the frontier.

Fetched and parsed recovery stages

For U17, the process records A9 before opening the connection. The body becomes P84, and manifest M3 contains U18 and U19. If it crashes after storing P84 but before completion, P84 is an orphan that can later be collected; U17's durable lease remains recoverable. If it crashes after completion but before enqueuing links, the manifest cursor shows which discovery work remains. A single “visited=true” bit would lose this distinction.

Breadth-first discovery and due recrawls

Use breadth-first order initially to spread discovery, with due-time priority for recrawls. Keep per-origin spacing even on one machine: one worker can still issue rapid sequential requests faster than a site's permitted start interval. DNS caching honors TTL; redirects are revalidated. This small version is already safe to restart and inspect. Distribution is an optimization for independent work, not a substitute for recording processing states.

architecture · baselineBaseline: one durable crawl loop

Visited state is a lifecycle, not a boolean; manifests preserve links across crashes.

Baseline: one durable crawl loopVisited state is a lifecycle, not a boolean; manifests preserve links across crashes. seed to worker: Enqueue eligible seed; worker to frontier: Lease URL and record stages; worker to web: Polite bounded HTTP fetch; worker to body: Store immutable bytes; worker to frontier: Commit links / completionEnqueue eligible seedLease URL and record stagesPolite bounded HTTP fetchStore immutablebytesCommit links / completionACTORSeed / operator inputSERVICEScheduler and fetchworkerSTOREDurable frontier /manifestsEXTERNALPublic web originsSTOREStored responsebodiessync
Read each connection in order
  1. syncEnqueue eligible seedSeed / operator input → Scheduler and fetch worker
  2. syncLease URL and record stagesScheduler and fetch worker → Durable frontier / manifests
  3. syncPolite bounded HTTP fetchScheduler and fetch worker → Public web origins
  4. syncStore immutable bytesScheduler and fetch worker → Stored response bodies
  5. syncCommit links / completionScheduler and fetch worker → Durable frontier / manifests

08Find the baseline flaws

The durable manifests in the baseline already address the crash case below. The remaining throughput problem motivates more workers, while the counterexamples show which safeguards must survive that change and which new coordination is needed across workers.

Bottleneck / counterexample Evidence and design consequence
Blocking fetch throughput At one second per fetch, a blocking worker processes roughly one page/s. Reaching 6,200/s needs concurrency across many origins, and storing over 600 MB/s challenges one disk/network path. Loading the entire billion-record frontier into memory is also a poor fit. These are throughput and capacity problems with straightforward partitioning opportunities.
Lost discovery after a crash The correctness counterexample is more subtle. Worker A downloads U17, discovers U18, sets visited and crashes before enqueuing U18. On restart, the crawler skips U17 and permanently misses the link. Reversing operations can create duplicate processing instead. The fix is a durable parse manifest and idempotent discovery consumption, not a belief that the worker will rarely crash.
Independent workers violate politeness Now add two workers without shared host scheduling. Both see example.org due at noon and start simultaneously; each locally obeys one request at a time, but the origin sees two. A lease timeout creates the same problem if a supposedly dead worker still has an active socket. We therefore separate frontier work ownership from permission to issue network traffic and explicitly fence old dispatchers. The tests must include a paused worker that resumes after lease expiry, not only a process that cleanly exits.

09Improve the design, step by step

  1. Change 1 — persistent partitioned frontier with ready batches. Trigger: billions of pending URLs and a single queue bottleneck. Store full durable state by origin partition, while buffering small enqueue/dequeue batches in memory. This reduces random I/O and supports parallel origins. Costs include queue indexes and recovery checkpoints; a lost ready buffer delays work but cannot lose its durable record. A single embedded database is simpler for a small crawl.

  2. Change 2 — asynchronous fetchers behind per-origin dispatch. Trigger: 6,200 required sockets and slow-response tails. Nonblocking fetchers start work only when both the origin’s policy and the crawler’s total connection limit permit it. Throughput increases across origins while one origin remains polite. The cost is lease/egress coordination and more sockets; a replaced dispatcher can overload a host unless the egress gate prevents it from starting more requests. More unconstrained threads are rejected because they do not solve origin scheduling.

  3. Change 3 — durable body and parsing pipeline. Trigger: parsing and body writes keep network workers occupied. Fetchers write immutable objects and stage manifests; parser workers consume references asynchronously. This isolates CPU-heavy extraction and permits reprocessing. Costs are extra storage reads and queue latency; malformed or adversarial HTML can repeatedly crash or stall parsers unless parsing is isolated with resource limits and retries are bounded. Inline parsing remains appropriate when pages are tiny and throughput modest.

  4. Change 4 — exact membership plus approximate acceleration. Trigger: 62,000 link candidates/s cause repetitive exact lookups. A Bloom filter can quickly identify definite negatives; possible positives still consult the exact URL store when coverage matters. Digest-based body deduplication avoids repeated extraction of mirrors. Costs include filter rebuilds, hashes and extra stores; treating Bloom positives as final can silently drop unseen pages. Choose approximate-only discovery only when the product explicitly accepts that recall loss.

10Detailed architecture

Discovery and scheduling ownership

Seeds and discovered links enter a URL gate that normalizes safe equivalents, checks scope and consults exact membership. Eligible work reaches the persistent frontier. Its scheduler chooses due origins and leases attempts, while a policy service supplies robots and site limits. Fetchers send actual network traffic through an egress gate that enforces destination safety and per-origin dispatch authority.

Eligibility is enforced at network dispatch

The final architecture shows these as separate components because a lease to process U17 is not automatically permission to open a new socket to example.org. The egress layer owns active outbound connections and validates the current dispatch ownership version, called an epoch. On failover, the old network authority must be stopped/fenced, or the new one must conservatively wait through the maximum in-flight timeout before granting conflicting work. A stale application token alone cannot revoke a socket already open on an unfenced machine.

Replayable bytes and extracted manifests

Downloaded bytes go to immutable object storage. A saved fetched-stage record tells parsers which body to process. Parsers save versioned output manifests, send discovered links through the URL gate and send extracted documents to the indexer. A digest index supports global content deduplication. Replicated frontier state and checkpoints preserve pending work; consistent hashing merely helps assign partitions with less movement.

Control changes versus body traffic

Control and data have different scaling: changing a site pause is low-volume but must reach egress enforcement promptly, while link discovery and object writes dominate volume. A paused site can retain its queued URLs without issuing more network requests.

Concrete starting stack

A practical initial stack can use a transactional SQL or embedded database for URL state and enqueue intentions, a bounded asynchronous HTTP client behind the egress gate, and object storage for immutable bodies. A durable message broker is useful when independent fetch/parse pools justify it, but it does not replace state transitions or outbox recovery. DNS safety checks must govern the address actually used by the connection: validate all resolved IPv4/IPv6 destinations and pin an approved address through connect while preserving the correct HTTP host and TLS name. Revalidate redirects and new resolutions; checking one DNS lookup but letting the connection use a second unchecked lookup allows DNS rebinding: the hostname can resolve to an approved public address during the check and an internal address during connection.

architecture · finalFinal: frontier, egress and replayable processing

Scheduling tokens control durable work; a separately fenced egress path controls actual network starts.

Final: frontier, egress and replayable processingScheduling tokens control durable work; a separately fenced egress path controls actual network starts. seeds to gate: 1. Seeds / policy changes; gate to exact: 2. Commit URL + enqueue intention; gate to frontier: 3. Replay idempotent pending job; frontier to scheduler: 4. Select due origin / URL; scheduler to policy: Read current site rules; scheduler to fetch: 5. Lease attempt and epoch; fetch to egress: 6. Request safe dispatch grant; egress to policy: Validate rules / epoch / spacing; egress to web: 7. Bounded HTTP request; fetch to objects: 8. Store response bytes; fetch to parseq: 9. Commit fetched reference; parseq to parser: 10. Lease parse stage; parser to objects: Read replayable body; parser to digest: Check digest and provenance; parser to gate: 11. Resolve URL-specific links durably; parser to sink: 12. Publish extracted document; parser to frontier: Complete manifest / recrawl due1. Seeds / policy changes2. Commit URL + enqueueintention3. Replay idempotent pendingjob4. Select due origin / URLRead current siterules5. Lease attempt and epoch6. Request safe dispatch grantValidate rules / epoch / spacing7. Bounded HTTP request8. Store response bytes9. Commit fetched reference10. Lease parse stageRead replayable bodyCheck digest and provenance11. Resolve URL-specific linksdurably12. Publish extracted documentComplete manifest / recrawldueACTORSeeds / operatorsG1SERVICEURL normalization /scope gateG1STOREExact URLmembership storeG1QUEUEPersistent frontier /leasesG1SERVICEOrigin schedulerG1STORERobots / origin policystoreG1WORKERBounded fetchworkersG2SERVICEFenced per-originegress gateG2EXTERNALPublic web originsG2STOREImmutable rawobject storeG3QUEUEFetched / parse stagerecordsG3WORKERSandboxed parserworkersG3STOREGlobal content digestindexG3EXTERNALDocument indexingsinkG4syncasyncG1 Durable discovery and schedulingG2 Network authorityG3 Stored bytes and replayG4 Search consumer
Read each connection in order
  1. sync1. Seeds / policy changesSeeds / operators → URL normalization / scope gate
  2. sync2. Commit URL + enqueue intentionURL normalization / scope gate → Exact URL membership store
  3. async3. Replay idempotent pending jobURL normalization / scope gate → Persistent frontier / leases
  4. sync4. Select due origin / URLPersistent frontier / leases → Origin scheduler
  5. syncRead current site rulesOrigin scheduler → Robots / origin policy store
  6. sync5. Lease attempt and epochOrigin scheduler → Bounded fetch workers
  7. sync6. Request safe dispatch grantBounded fetch workers → Fenced per-origin egress gate
  8. syncValidate rules / epoch / spacingFenced per-origin egress gate → Robots / origin policy store
  9. sync7. Bounded HTTP requestFenced per-origin egress gate → Public web origins
  10. sync8. Store response bytesBounded fetch workers → Immutable raw object store
  11. async9. Commit fetched referenceBounded fetch workers → Fetched / parse stage records
  12. async10. Lease parse stageFetched / parse stage records → Sandboxed parser workers
  13. syncRead replayable bodySandboxed parser workers → Immutable raw object store
  14. syncCheck digest and provenanceSandboxed parser workers → Global content digest index
  15. async11. Resolve URL-specific links durablySandboxed parser workers → URL normalization / scope gate
  16. async12. Publish extracted documentSandboxed parser workers → Document indexing sink
  17. syncComplete manifest / recrawl dueSandboxed parser workers → Persistent frontier / leases

11Write path and acknowledgement

Each fetch produces durable artifacts that later stages can replay. In this example, URL U17 is fetched under lease token L9, produces body P84, and yields manifest M3 containing discovered URLs U18 and U19.

At noon, the scheduler admits the example fetch under the following checks.

  1. Worker w3 leases U17 as L9 and checks host eligibility/robots rules.
  2. DNS resolves the destination; the fetcher downloads its body once under byte/time limits.
  3. A replayable document input stream lets processors reread the bytes: small bodies stay in RAM; large bodies spool to a temporary file.
  4. Content hash H4 and object ID P84 identify the result. New content proceeds to parsing; duplicate bytes may reuse context-independent parse output, but URL-relative link resolution still runs for each fetched URL.
  5. The parser resolves ./2 and ../about to absolute U18/U19; the URL gate filters, checks membership, and durably enqueues new addresses.
  6. Completion records P84 and releases L9.

Future image/video MIME handlers can reuse the stream abstraction without pretending their contents are HTML.

Before the network step, the egress gate validates the current origin epoch, safe resolved destination, current robots decision and due time. It reserves the next allowed start and active slot under one authority. A redirect repeats safety/policy checks for its target; it cannot tunnel into a private address because the initial URL was public.

After body storage, the fetched-stage record refers to P84 and the durable attempt identified by lease token L9. A parser writes M3 before completing its stage. A discovery consumer inserts U18/U19 with unique canonical URL keys, records its manifest position and retries safely after a crash. If another page already discovered U18, that insertion returns the existing URL record rather than creating another frontier entry. Completion and manifest consumption are separate recoverable stages, not a distributed transaction across the object store and URL database.

The output document includes URL provenance, fetch timestamp and extraction version. Identical content can share a body object while retaining separate fetch records. That lets a later extractor fix a bug or a recrawl update metadata without fabricating a new network observation.

12Read and delivery path

Workers obtain due work from durable scheduler state and retrieve stored bodies by reference. The path below distinguishes scheduling reads, network eligibility and parser reprocessing.

  1. A scheduler reads its next due origin from a durable time/priority index. If example.org is paused, its work remains pending and the scheduler moves to another origin rather than spinning on it.
  2. It reads the origin's policy version and selects a due URL whose retry/revisit time has arrived. Within a short transaction, it changes pending to leased, increments attempt and returns L9 with a deadline.
  3. The fetcher reads cached robots rules only within their valid policy; missing or expired rules schedule a compliant refresh rather than assuming unrestricted access. Robots error behavior follows the chosen RFC-compliant implementation.
  4. DNS resolution uses TTL-aware caching, then the egress layer verifies actual destination addresses and redirect targets. The fetch reads bytes under time and expansion limits; small replayable streams stay in memory and larger ones spool.
  5. Parser workers read P84 by object reference and read their extraction-generation manifest state. They can reprocess the same bytes without issuing another HTTP request.
  6. Operator status reads aggregate frontier age, recent result and next-due time. They do not mutate visited state or change a worker's lease merely because a dashboard page was opened.

This read path is largely scheduling and object retrieval, not user-facing search. The search engine is a downstream consumer with its own indexing freshness contract.

Robots retrieval has explicit outcomes. A successful response is parsed for the crawler’s user agent. RFC 9309 permits access when robots is unavailable through an HTTP 4xx response, but this crawler still honors throttling and applies backoff for 429. For server/network failure, use the RFC’s unreachable handling; this design conservatively stops new fetches while policy cannot be established. Cached rules normally should not be used beyond 24 hours unless the RFC’s unreachable exception applies. Any redirect used while retrieving robots is still subject to destination-safety controls. A robots allow never authorizes access to a private network.

13Correctness deep dive

Deduplication answers two different questions: have we scheduled this URL, and have we processed these response bytes? URL identity controls discovery, while byte identity can save storage and parsing work. The table separates the authoritative records from filters that only accelerate lookups.

Mechanism Meaning Cost or error
Canonical URL store This address was scheduled Stores addresses/index overhead
Content digest store These bytes were processed Needs collision handling
Bloom filter before exact lookup Negative means definitely absent Positive may be false
Bloom filter as sole gate Small approximate visited set Can permanently skip unseen pages

Bloom filters cannot prove completeness

Explicit attempt transitions

Use explicit attempt transitions rather than a boolean visited flag:

Transition Guard checked atomically Durable effect
pending → leased Due now; no valid attempt; current scheduler epoch New attempt/token/deadline
leased → fetched Token matches current attempt Body reference and fetch metadata
fetched → parsed Matching body/extraction generation Durable manifest M3
parsed → complete Manifest safely published for consumption Completion and recrawl due time
lease expiry → pending Deadline passed; attempt still incomplete Retry due; old token becomes stale

Stale fetch completion is rejected

Worker w3 stores P84 and pauses before recording fetched. Its lease expires and w8 gets L10. If w3 resumes, complete(L9,...) compares its token with L10 and rejects the stale mutation. W8 may fetch identical bytes; digest deduplication can reuse storage, but the newer attempt remains authoritative. If w3 had committed fetched before pausing, recovery continues parsing P84 without downloading again. The database transaction decides which durable state exists; wall-clock guesses about the worker do not.

recordFetched(urlId, token, bodyRef):
  update URL
    set state=FETCHED, body=bodyRef
    where id=urlId and state=LEASED and currentToken=token
  require one row changed, or return STALE_ATTEMPT

Atomic discovery and enqueue intention

Equal bytes can yield different resolved links

Body collection versus publication

Cleanup must not delete a body just as a worker saves its reference. Track each staged object under its fetch/parse attempt. The state database either commits the reference or marks the attempt RECLAIMING, blocking later publication before cleanup deletes the bytes. Garbage collection skips bodies and manifests reachable from committed stages. Merely finding no reference in one scan and deleting afterward can race a worker recording FETCHED.

sequence · stale-attemptA paused worker cannot overwrite a newer fetch

Attempt-token guards protect durable state; egress fencing separately prevents stale network starts.

A paused worker cannot overwrite a newer fetchAttempt-token guards protect durable state; egress fencing separately prevents stale network starts. db to w3: Lease U17 / L9; w3 to objects: Store P84; then pause; db to db: Expire L9; lease L10; db to w8: Return U17 / L10; w3 to db: Late recordFetched(L9,P84); db to w3: Reject stale token; w8 to objects: Store/reuse fetched body; w8 to db: recordFetched(L10,body); db to w8: Commit current attemptPARTICIPANTWorker w3PARTICIPANTFrontier authorityPARTICIPANTObject storePARTICIPANTWorker w81. Lease U17 / L92. Store P84; then pause3. Expire L9; lease L104. Return U17 / L105. LaterecordFetched(L9,P84)6. Reject stale token7. Store/reuse fetched body8. recordFetched(L10,body)9. Commit current attemptreturnsync
Read each connection in order
  1. returnLease U17 / L9Frontier authority → Worker w3
  2. syncStore P84; then pauseWorker w3 → Object store
  3. syncExpire L9; lease L10Frontier authority → Frontier authority
  4. returnReturn U17 / L10Frontier authority → Worker w8
  5. syncLate recordFetched(L9,P84)Worker w3 → Frontier authority
  6. returnReject stale tokenFrontier authority → Worker w3
  7. syncStore/reuse fetched bodyWorker w8 → Object store
  8. syncrecordFetched(L10,body)Worker w8 → Frontier authority
  9. returnCommit current attemptFrontier authority → Worker w8

14Failure and recovery

Failure / interleaving Required response and recovery
Crash after download Worker w3 downloads P84, then crashes before acknowledging L9. After lease expiry, w8 retries U17. This duplicate fetch is acceptable; idempotent completion/content processing prevents duplicate records. Persist frontier transitions and periodic checkpoints so recovery restores both pending URLs and known results.
Replacement origin scheduler Assign host scheduling to one fenced owner: a replacement must invalidate old ownership, not merely start alongside it. One FIFO or one thread does not enforce spacing; store next-eligible times and active-fetch limits. Define an origin as scheme, host and port. Different origins can share an IP or operator infrastructure, so add a conservative shared-server budget where appropriate. Consistent hashing reduces routing movement, while replicated queues/checkpoints actually preserve work after a machine loss.
Partitioned egress owner If an egress owner is partitioned rather than dead, stop new grants until its dispatch authority is fenced. A replacement that simply increments an application epoch while the old machine continues networking cannot claim strict one-active-request behavior. Use infrastructure-enforced exclusive egress ownership or a conservative timeout/quiescence handoff. Residual network packets may still arrive late; define start spacing and maximum active duration in terms the implementation can enforce.
Storage, parsing or discovery overload If object storage is unavailable, do not mark fetched/complete with an invented body reference. Backpressure the fetch fleet before filling local disks. If parsing falls behind, preserve fetched objects and prioritize existing backlog rather than endlessly downloading new bodies. If one site's URLs explode, quarantine that origin's discovery budget without blocking other origin partitions. On a global queue restore, replay manifests and exact insertions idempotently; duplicate discovery is safer than unrecorded missing links.

15Operations, security, and cost

Bound crawl traps

Calendar pages, sorted/filter combinations, session URLs, symbolic cycles, spam, and deliberate traps can create endless branches. Bound depth, URL length, redirects, per-site discovery, retries, and response expansion. Google documents how faceted navigation can generate excessive crawlable URL combinations. Crawler guidance.

Destination validation and parser isolation

Reject internal/private destinations and recheck DNS/redirects to prevent server-side request forgery (SSRF), where an attacker makes the crawler contact a destination the attacker could not access directly. Sandbox parsing; protect against compression bombs. Respect throttling and back off. Measure new-page yield, duplicate rate, frontier age, per-host spacing, DNS/fetch latency, error codes, checkpoint lag, and recrawl freshness.

Useful-content and freshness signals

Measure useful new documents per fetched byte, recrawl freshness by priority class, and robots/pause enforcement latency. A high fetch count can hide calendar traps and duplicate mirrors. Track the maximum observed per-origin start rate, not only fleet average. Preserve enough audit metadata to explain why a URL was skipped without logging secret credentials or unrestricted response bodies.

Parsing and retention cost

At 620 MB/s ingress, a parser that rereads every body twice consumes another 1.24 GB/s of storage-read traffic. A replayable stream abstraction avoids duplicate network downloads but does not make repeated object reads free. If content deduplication avoids context-independent parsing for 30% of bytes, it can save roughly 186 MB/s of that parse input under the illustrative workload, at the price of digest computation and lookup traffic. Validate with actual duplicate distribution.

Canonicalizer rollout and replay tests

Canary a new canonicalizer against stored URLs before changing identity rules; an overaggressive query-parameter removal can merge distinct articles permanently. Roll out a new parser generation on existing objects, compare extracted links and document quality, then enable it for new fetches. Recovery tests pause a worker after every stage, corrupt a checkpoint copy and change robots policy while URLs are waiting.

16Decision ledger and limitations

Choice Benefit Cost or limit Change trigger
Origin-partitioned scheduling Coordinated politeness Hot origins cannot use unlimited workers Site grants a different fetch budget
At-least-once fetch attempts Recoverable network uncertainty Duplicate downloads are possible No general exactly-once HTTP alternative exists
Durable raw bodies/manifests Reprocessing and discovery recovery Petabyte storage and read cost Link-check-only product can discard bytes sooner
Bloom filter plus exact store Faster repeated membership checks More components and filter memory Approximate coverage is explicitly acceptable
Separate fetch and parse stages Resource isolation Additional queues and storage reads Small crawl favors one restartable process

Breadth-first traversal spreads discovery; depth-first traversal may reuse connections and memory locality but risks spending too long in one branch. Priority scheduling beats either blindly when revisit freshness and site importance matter. Global content deduplication saves work across mirrors but must preserve URL-specific provenance. Hash fingerprints require collision analysis and, where completeness matters, exact verification.

The limiting resource may be site permission rather than our infrastructure. When the eligible corpus cannot sustain 6,200 polite fetches/s, revise the four-week goal or scope. Do not call that an autoscaling failure. Similarly, no scheduler can guarantee exhaustive coverage of endlessly generated calendars and faceted URLs; scope budgets make the objective finite and measurable.

17Interview closing

“I model the crawler as a durable frontier and a recoverable processing pipeline. A URL moves through leased, fetched, parsed and complete states, each with a token and durable artifacts. The scheduler partitions by origin, while egress authority enforces robots, safe destinations, spacing and active-request limits. Fetchers store bytes once, parsers produce replayable manifests, and exact URL insertion makes repeated discovery harmless. A stale worker cannot overwrite a newer attempt; a crash may repeat a fetch but cannot silently lose its links.

“I scale independent origins with bounded asynchronous I/O and persistent frontier batches. The costs are duplicate network work, petabyte body storage and coordination around polite failover. More workers do not increase one site's permitted rate. I would next measure the fraction of fetches yielding useful new pages, the number of independently eligible origins, the fetch slots occupied by slow requests, and recovery of a worker paused after body storage.”

If the interviewer changes the goal to continuously monitoring a small set of sites, prioritize revisit scheduling and conditional fetches over broad discovery. If media crawling is added, separate MIME handlers and byte budgets change capacity; the durable stages and destination-safety checks remain. If approximate coverage is acceptable, Bloom-only rejection can be a conscious recall tradeoff, not an unnoticed correctness bug.

Practise the interview questions

Say your answer aloud before opening the model answer. Then answer the follow-up and compare the reasoning.

Foundation · Question 1

What records and components make the smallest restartable web crawler?

Reveal a model answer

I start with a durable frontier of eligible URLs, a bounded fetcher, a parser, exact URL membership and stored response bodies or replayable results. Robots, destination safety and per-origin scheduling gate the fetch. A persisted processing state and parse manifest let recovery distinguish a downloaded page from discovered links that still need enqueuing. One worker is enough to prove that lifecycle before distributing it.

What the answer must demonstrate: Explain data flow rather than list service names.

Applied · Question 2

We use one FIFO per site. Is that enough to avoid overload?

Reveal a model answer

No. A FIFO defines order but could still issue hundreds of fast requests every second. I record the next allowed fetch time and active-request limit for the site, and the scheduler chooses only hosts currently eligible under that policy.

What the answer must demonstrate: Queue order is not rate control.

Foundation · Question 3

Would you put a Bloom filter in front of the URL database?

Reveal a model answer

Yes, as an optimization: definite negatives skip the lookup, while positives are checked against the exact store when coverage matters. Using positives as final proof of prior visitation would intentionally skip some new URLs because false positives exist.

What the answer must demonstrate: Do not confuse compact fingerprints with unique identifiers.

Applied · Question 4

A worker fetched a page but died before marking it complete. What happens?

Reveal a model answer

Its lease expires and another worker retries. I accept the possible repeated download and make completion/content processing idempotent. The durable frontier tells recovery the URL is still unfinished; a transient worker flag would lose it or leave it stuck forever.

What the answer must demonstrate: Fetching once and processing once are different guarantees.

Follow-up · Question 5

How do you know the entire web has been crawled?

Reveal a model answer

I cannot make that claim for a changing, potentially unbounded graph. I report coverage of discovered eligible URLs under a budget, plus freshness for prioritized pages. New content, disconnected resources, and infinitely generated URLs make a global finished flag misleading.

What the answer must demonstrate: Define a measurable crawl goal.

Follow-up · Question 6

Two different domains serve identical articles. Which dedupe finds them?

Reveal a model answer

URL dedupe does not, because the addresses differ. After downloading, a global digest store can share identical bytes and context-independent parse work. I retain each URL’s metadata and still resolve relative links against its effective URL; identical HTML on two domains can discover different child addresses. I verify collisions rather than treat the digest as proof of identity.

What the answer must demonstrate: Share bytes without erasing URL-specific link resolution, policy or provenance.

Follow-up · Question 8

Does rejecting a stale completion token guarantee polite fetching?

Reveal a model answer

No. It protects frontier state, but an old worker may still open a network connection. Actual dispatch must pass a current origin/egress authority, and failover must fence old network authority or wait conservatively for in-flight requests to end.

What the answer must demonstrate: Separate durable work ownership from actual outbound traffic.

Blank-page exercise · 45 minutes

Build the answer yourself

Design a public HTML crawler targeting 15 billion eligible fetches in four weeks. Derive frontier, network and storage capacity; enforce origin policy; recover a worker failure after download; and bound discovery on an infinite calendar site.

  • Draw the complete URL-discovery loop.
  • Compute fetch rate, concurrency, network, and storage.
  • Separate URL and content membership checks.
  • Enforce a real host request budget.
  • Recover leased work and define trap/SSRF defenses.

Check that each component and design decision follows from your requirements and workload.

Recall the key ideas

Answer from memory before opening each card. Explain why the choice works and what it costs. Revisit missed cards tomorrow.

Design a web crawlerWhy do we deduplicate twice?Recall first, then reveal

Seen URLs avoid unnecessary downloads; identical document content avoids duplicate processing after download.

Before: address. After: bytes.

Return to lesson
Design a web crawlerWhat makes a fetch polite?Recall first, then reveal

An enforced host/origin request budget and concurrency rule, not just a FIFO queue.

Eligibility before execution.

Return to lesson
Design a web crawlerWhat lets a crawl resume?Recall first, then reveal

Durable frontier records, expiring leases, completed-work records, and checkpoints.

Remember pending, owned, and done.

Return to lesson

Final revision

Summary and interview notes

A reliable crawler remembers unfinished URLs, enforces each origin’s request limits and saves extracted links for replay. A failed HTTP attempt may repeat, but recovery must not lose discovered links or send unlimited work to one site.

Remember these points

  • URL membership and the enqueue intention commit together; a separate queue send cannot be the only record of pending work.
  • Frontier attempt tokens protect stored state, while an actual egress enforcement point protects origin spacing and concurrency.
  • Identical bytes can share storage and parsing, but relative links still resolve under each fetched URL’s context.
  • Bloom filters accelerate membership; exact keys enforce uniqueness when coverage matters.
  • Crawl throughput is limited by eligible origin budgets as well as network and storage capacity.

Interview tips

  • Walk a crash after body storage and another after manifest publication but before child enqueue.
  • Calculate the number of independent origins needed under the chosen start interval.
  • Use a concrete calendar or faceted-URL trap to explain bounded discovery and measurable coverage.

Important qualifications

  • Robots policy is not access authorization; network destination validation still applies to redirects and DNS resolution.
  • Exactly-once HTTP fetching is not promised, and retained body references must be protected against cleanup races.
  • A changing unbounded web has no dependable global completion flag; report eligible coverage and revisit freshness instead.

Technical references

Practice marks stay in this browser.