MediumDepth-First SearchBreadth-First SearchConcurrencyUpdated Sep 2026

Web Crawler Multithreaded

Asked at Anthropic

Difficulty

Medium

Time

O(N * F)

Space

O(N)

Pattern

Depth-First Search

Companies

1

Problem

You are given a starting URL startUrl and an object HtmlParser. That object has one method, getUrls(url). It returns every link found on the page at url. This call blocks: it waits until the page is fetched, but it always finishes within 15ms.

Write a multi-threaded crawler. Start at startUrl, call getUrls on each page you visit, and follow the links you find. Two rules limit what you follow: never visit the same URL twice, and only follow links whose hostname equals the hostname of startUrl. Links to other hosts are ignored.

Return the list of all URLs you crawled, in any order. The list must include startUrl itself. A single-threaded crawler is too slow for the time limit, so you must fetch pages from several threads at the same time.

Which links may we follow?

start: news.yahoo.com/news/topics/links to news.yahoo.com/newslinks to news.yahoo.comlinks to news.google.comGreen links share the host; the red link is dropped
Only pages on the start host count. The google page is reachable but must be skipped.

Constraints

  • ●1 <= urls.length <= 1000
  • ●1 <= urls[i].length <= 300
  • ●startUrl is one of the urls
  • ●Hostname labels are 1 to 63 characters, using only a-z, 0-9 and '-'
  • ●A hostname may not start or end with '-'
  • ●All URLs use http with no port
  • ●There are no duplicate URLs in the library

Examples

Example 1
Inputurls = ["http://news.yahoo.com","http://news.yahoo.com/news","http://news.yahoo.com/news/topics/","http://news.google.com","http://news.yahoo.com/us"] edges = [[2,0],[2,1],[3,2],[3,1],[0,4]] startUrl
Output["http://news.yahoo.com","http://news.yahoo.com/news","http://news.yahoo.com/news/topics/","http://news.yahoo.com/us"]
WhyThe start page links to the two yahoo pages and to news.google.com. The google page is a different host, so we skip it. From the yahoo pages we reach the remaining yahoo URLs. All four yahoo URLs are reachable and share the host, so all four are returned.
Example 2
Inputurls = ["http://news.yahoo.com","http://news.yahoo.com/news","http://news.yahoo.com/news/topics/","http://news.google.com"] edges = [[0,2],[2,1],[3,2],[3,1],[3,0]] startUrl = "http://news.google.com"
Output["http://news.google.com"]
WhyThe start page is on news.google.com. Every link it offers points to news.yahoo.com, which is a different host. Nothing else is allowed, so the answer is just the start URL.

Asked At

1 companies
CompanyDifficulty
AnthropicMediumView all Anthropic questions →

How to Think About It

1

The core algorithm is BFS: start from the seed URL, fetch the page, extract all links, add unvisited links to a queue, and repeat. The multithreading part is running multiple BFS workers concurrently.

2

Data structures: a queue (thread-safe) for URLs to visit, a set for visited URLs (to avoid duplicates and cycles), and a list for results. The queue is shared across threads.

3

Thread safety: the visited set and queue are shared state. Use a lock (mutex) to protect concurrent access. Alternatively, use thread-safe data structures like ConcurrentHashMap in Java or queue.Queue in Python.

4

BFS with thread pool: create a fixed number of threads (e.g., equal to CPU cores or a small constant). Each thread loops: grab a URL from the queue (under lock), fetch the page, extract links, add new links to queue (under lock), mark as visited.

5

Stopping condition: when the queue is empty and all threads are idle, stop. Use a threading.Event or Condition to signal completion. Each thread checks if the queue is empty before proceeding.

6

Complexity: time is O(N * F) where N = number of URLs, F = average fetch time. Space: O(N) for visited set and queue. The threading does not change Big-O but reduces wall-clock time by a constant factor.

Optimal Approach

Use BFS with a thread pool. Maintain a thread-safe queue of URLs to visit and a set of visited URLs.

Algorithm:

  1. Initialize: queue = [startUrl], visited = {startUrl}, results = [].
  2. Create N worker threads (e.g., 4).
  3. Each worker thread loops:
    a. Lock and check if queue is empty. If yes, break.
    b. Dequeue a URL.
    c. Fetch the page and extract all links.
    d. For each link with the same hostname:
    • Lock and check if not in visited.
    • Add to visited and enqueue.
      e. Add the URL to results.
  4. When all threads finish, return results.

Walkthrough with startUrl = "http://example.com":

Time: O(N * F) where N = URLs, F = fetch time. Space: O(N).

Shared queue, many workers

queue/news/topics/frontbackStart: only the seed URL is waitingworker locks queuepop a URLgetUrls(url) — blockinglock, add new linksEach worker repeats this loop until the queue is emptyvisited = {startUrl}if link not in visited: visited.add(link); queue.append(link)
Workers pull URLs from one queue and push new same-host links back, protected by a lock.
ComplexityTime O(N * F)Space O(N)N is the number of same-host URLs and F is the average time of one getUrls call; threads cut the wall-clock time but not the total work.

What Trips People Up in Real Interviews

1

Not using proper synchronization on the shared visited set. If two threads check the same URL simultaneously, both might add it. Use a lock around the check-and-add operation, or use a concurrent set.

2

Creating too many threads. The thread pool should be fixed-size (e.g., 4-8 threads). Creating a new thread per URL causes thread explosion and context-switching overhead.

3

Forgetting to handle the case where the queue is empty but threads are still working. Use a condition variable or event to signal all threads to stop when no more URLs remain.

4

Not handling fetch failures gracefully. If a URL fails to fetch (timeout, 404, etc.), skip it and move on. Do not let one failed fetch block the entire crawler.

5

Confusing BFS with DFS. BFS is preferred for crawling because it explores breadth-first (closer pages first) and naturally terminates when the queue is empty. DFS can get stuck in deep chains.

Solution Code

O(N * F) time
import threading
from urllib.parse import urlparse

class Solution:
    def crawl(self, startUrl: str, htmlParser: 'HtmlParser') -> list[str]:
        hostname = urlparse(startUrl).hostname
        visited = set()
        visited.add(startUrl)
        queue = [startUrl]
        lock = threading.Lock()
        results = []
        done = threading.Event()

        def worker():
            while True:
                url = None
                with lock:
                    if queue:
                        url = queue.pop(0)
                    else:
                        done.wait(timeout=0.1)
                        if queue:
                            url = queue.pop(0)
                        elif done.is_set():
                            return
                        else:
                            continue
                pages = htmlParser.getUrls(url)
                with lock:
                    results.append(url)
                for link in pages:
                    if urlparse(link).hostname == hostname:
                        with lock:
                            if link not in visited:
                                visited.add(link)
                                queue.append(link)

        threads = [threading.Thread(target=worker) for _ in range(4)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()
        done.set()
        return results

Pro at DSA?

Test your skills with a real FAANG-style mock interview.

Start a Mock Interview →

Frequently Asked Questions

What is the Web Crawler Multithreaded problem?

Design a multithreaded web crawler that starts from a given URL and crawls all URLs within the same hostname. The crawler should use multiple threads to fetch pages concurrently and return a list of all crawled URLs. This problem tests your ability to combine BFS with thread pool management.

How do you solve Web Crawler Multithreaded?

The optimal approach runs in O(N * F) time and O(N) space. Scroll up to "Optimal Approach" for the step-by-step walkthrough, diagrams, and code in Python, C++ and Java.

What companies ask Web Crawler Multithreaded?

Web Crawler Multithreaded is asked at Anthropic. It is a medium difficulty problem.

What are common mistakes on Web Crawler Multithreaded?
  • Not using proper synchronization on the shared `visited` set. If two threads check the same URL simultaneously, both might add it. Use a lock around the check-and-add operation, or use a concurrent set.
  • Creating too many threads. The thread pool should be fixed-size (e.g., 4-8 threads). Creating a new thread per URL causes thread explosion and context-switching overhead.
  • Forgetting to handle the case where the queue is empty but threads are still working. Use a condition variable or event to signal all threads to stop when no more URLs remain.
  • Not handling fetch failures gracefully. If a URL fails to fetch (timeout, 404, etc.), skip it and move on. Do not let one failed fetch block the entire crawler.
  • Confusing BFS with DFS. BFS is preferred for crawling because it explores breadth-first (closer pages first) and naturally terminates when the queue is empty. DFS can get stuck in deep chains.