Traverse All Reachable Pages With Parallel BFS
SnowflakeSoftware Engineer · Technical ScreenLast updated
Implement a concurrent breadth-first web crawler that discovers all reachable pages from a start URL. The crawler must fetch pages concurrently while maintaining deterministic level-order output and safely handling cycles and fetch errors.
Problem Statement
You are tasked with building a web crawler that traverses all web pages reachable from a given start_url. Fetching a webpage over the network is high-latency, so your crawler must issue fetch requests concurrently while guaranteeing a deterministic discovery order.
You are provided an interface Fetcher:
class Fetcher:
def get_links(self, url: str) -> list[str]:
"""Returns the list of outgoing URLs from `url`.
Raises an Exception if the network request fails.
"""
pass
Implement the function crawl(start_url: str, fetcher: Fetcher, max_concurrency: int) -> list[str].
Requirements
- Deterministic Level-Order Traversal: Return URLs in breadth-first search (BFS) order. Level 0 contains
[start_url]. Levelkcontains all newly discovered, unvisited URLs directly reachable from pages in Levelk - 1. Within each level, URLs must be ordered lexicographically. - Cycles and Duplicates: Each unique URL must be fetched at most once and must appear in the output list exactly once.
- Concurrency: At most
max_concurrencycalls tofetcher.get_linksshould be active at any given moment. - Fault Tolerance: If
fetcher.get_links(url)raises an exception, treat that page as having no outgoing links. The failed URL itself should still be included in the output list.
Function Signature
def crawl(start_url: str, fetcher: Fetcher, max_concurrency: int = 4) -> list[str]:
pass
Examples
Example 1
Graph Structure:
"pageA"->["pageC", "pageB"]"pageB"->["pageD"]"pageC"->["pageD", "pageA"]"pageD"->[]
Input:
start_url = "pageA"
max_concurrency = 2
Output:
["pageA", "pageB", "pageC", "pageD"]
Explanation:
- Level 0:
["pageA"] - Level 1:
["pageB", "pageC"](sorted lexicographically) - Level 2:
["pageD"]("pageA"is already visited)
Example 2
Graph Structure:
"root"->["child1", "child2"]"child1"raises aTimeoutError"child2"->["leaf"]
Input:
start_url = "root"
max_concurrency = 4
Output:
["root", "child1", "child2", "leaf"]
Constraints
1 <= total reachable pages <= 10,0001 <= max_concurrency <= 64- URL strings consist of printable ASCII characters with length between 1 and 256.
Hints
Hint 1
Process the graph level by level (bulk-synchronous parallel BFS) so that all URLs for a given level are known before fetching their children.
Hint 2
Use a thread pool or worker pool bounded by `max_concurrency` to fetch the URLs of the current level in parallel.
Hint 3
Maintain a global visited set protected by a lock (or updated strictly between BFS steps) to prevent scheduling duplicate fetches for the next level.
Follow-up questions
- How would you enforce a per-domain rate limit (e.g., maximum 2 requests per second per host) without stalling worker threads processing unrelated domains?
- If the graph has millions of nodes and cannot fit in memory, how would you externalize the visited set and queue across multiple machines?
- Instead of synchronizing strictly level-by-level, how would you implement an asynchronous queue with backpressure while still producing deterministic output?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.