How Does a Web Crawler Work?
Follow a crawl step by step: seeds, the frontier, fetching, link extraction, normalisation, deduplication, scheduling, and how a crawl terminates.
A crawler is a loop around two data structures: a queue of URLs to visit and a record of URLs already seen. Everything else β politeness, priority, persistence β is a refinement of that loop.
1. Seeds
A crawl starts from known URLs.
The best seed is almost always a sitemap. robots.txt usually points to one, and it hands you the site's own list of canonical URLs β no discovery traffic, no missed pages behind JavaScript navigation, no trap risk.
Failing that: the homepage, category indexes, or a search results page.
2. The Frontier
The frontier is the queue of URLs waiting to be visited. Its ordering determines crawl behaviour:
- FIFO gives breadth-first β level by level, and a sensible default.
- LIFO gives depth-first, which follows one branch deep and is easy to trap.
- Priority ordering visits by estimated importance β shallow paths, sitemap priority, or observed value.
At scale the frontier is not one queue but one per host, because rate limiting is per host. A single global queue either starves you (waiting on one slow host) or overloads someone (draining their URLs consecutively).
3. Fetching
Pop a URL, check politeness, and request it.
Before fetching, the crawler checks whether robots.txt permits the path, whether the per-host delay has elapsed, and whether budgets β depth, host page count, total pages β still allow it.
Responses split three ways:
- 2xx β parse it and extract links.
- 3xx β follow, and record the target as canonical. Cap redirect chains; loops exist.
- 4xx / 5xx β record the failure. Retry 5xx and 429 with backoff; do not retry 404.
4. Extracting Links
Every URL in the response is a candidate:
for a in tree.css("a[href]"):
yield urljoin(base, a.attributes["href"])
Sources beyond anchors matter too β <link rel="canonical">, pagination rel="next", and sitemap entries.
A limitation worth naming: links created by JavaScript are invisible to an HTTP-only crawler. If a site's navigation is client-rendered, discovery silently under-collects. The usual fix is not a browser but the sitemap, or the internal API that serves the navigation.
5. Normalisation
Before deduplication, each URL is canonicalised:
- Resolve relative paths against the base.
- Drop the fragment β
#reviewsis the same document. - Sort query parameters so ordering does not create duplicates.
- Strip tracking parameters β
utm_*,ref,fbclid. - Normalise the scheme and host case; leave the path case alone unless you know the server is case-insensitive.
- Decide on trailing slashes and apply it consistently.
Skipping this is the single most common reason a crawl never finishes.
6. Deduplication
Check the normalised URL against the seen-set. If present, discard. If not, record it and enqueue.
In memory a Python set is fine into the low millions. Beyond that, the options are a Bloom filter β constant memory, a small false-positive rate that silently skips a few pages β or a disk-backed key-value store, which is exact and slower.
Content-level deduplication is a separate concern: different URLs frequently serve identical content. Hashing the normalised body catches those, which matters when the same product is reachable through several category paths.
7. Termination
A crawl ends when the frontier empties β or, more often in practice, when a limit is reached. Every crawl needs explicit bounds:
- Max depth from the seed.
- Max pages, total and per host.
- URL pattern caps, so one parameterised path cannot dominate.
- Wall-clock timeout.
Without them, a calendar widget with a "next month" link generates unique, valid URLs forever, and the crawl never terminates. Bounds are cheap; discovering the need for them at 3 a.m. is not.
Persist the frontier and seen-set to disk. A crawl that cannot resume loses days of work to a single restart.
8. Summary
A crawler pops a URL from the frontier, checks politeness and budgets, fetches it, extracts links, normalises and deduplicates them, and enqueues what is new β repeating until the frontier empties or a limit stops it.
Normalisation is what makes termination possible, per-host queues are what make politeness workable, and explicit bounds are what protect the crawl from traps that generate infinite valid URLs.
Start from a sitemap when one exists: it gives the site's own canonical URL list without any discovery traffic, and it sidesteps the blind spot around JavaScript-generated navigation entirely.