Concurrent Web Crawler with Cats Effect Fibers
Build a web crawler in Scala with Cats Effect and sttp: starting from a page, it fetches pages concurrently as lightweight fibers, follows links on the same site up to a chosen depth, never visits a page twice, and limits both the total number of requests in flight and the rate per website. The HTTP client is a resource that is always closed properly, a failing page never stops the crawl, and pressing Ctrl+C cancels everything in progress cleanly and still writes what was found. Only crawl sites you own or have permission to crawl. You'll learn fibers and structured concurrency, Resource for safe cleanup, Ref and Semaphore for shared state and limits, fs2 streams for results, and testing concurrent code against a fake server.
- Estimate
- ~11.5h
- Steps
- 6
- Completed by
- 0
- Proposed by
- codeseed.app
Cats Effect · sttp · fs2
Project roadmap
- 01
Set up the HTTP client
~1.5hCreate an sbt project with Cats Effect, fs2, sttp (with a Cats Effect backend such as the fs2 or http4s backend) and jsoup for HTML parsing. Create the sttp backend as a Resource in an IOApp, with connection and read timeouts and a descriptive user agent, and fetch one page to check it works. Done when: the app fetches a page and prints its status, and the client is closed on exit.
- 02
Fetch a page and extract links
~1.5hWrite a function that fetches a URL, accepts only HTML responses up to a size limit, parses it with jsoup, and returns the title and links resolved to absolute URLs (dropping fragments, mailto links and non-http schemes). Normalize URLs so the same page always has the same form. Done when: unit tests extract the right links from saved HTML.
- 03
Crawl concurrently with fibers
~3hCrawl breadth-first: process each depth level with parTraverseN (or a Semaphore limiting how many fetches run at once), collect the new links, and continue to the next level until the depth limit or a maximum number of pages. Only follow links on the starting domain (or an allowed list). Done when: a local test site is crawled completely with at most N requests in flight.
- 04
Deduplicate and rate limit
~2hKeep visited URLs in a Ref[IO, Set[String]], adding each URL atomically with modify so two fibers can never both claim it. Limit requests per domain with a small per-domain rate limiter (for example a Semaphore plus a delay, or a token bucket in a Ref), and respect robots.txt. Done when: no page is fetched twice, and requests to one site are spaced out.
- 05
Handle errors and shut down cleanly
~1.5hCatch errors per page (timeouts, bad status codes and parse errors) with handleErrorWith and record them as results, so one failure never stops the crawl. Stream results with fs2 to a JSON Lines file as they arrive. On Ctrl+C, IOApp cancels the main fiber: make sure in-flight requests are cancelled, the output file is flushed and closed, and a summary is printed. Done when: interrupting a crawl leaves a complete output file and a summary.
- 06
Write tests
~2hWrite munit-cats-effect tests against a fake server (an http4s server in the test or sttp's stub backend) serving a site with loops, broken links, slow pages and external links: link extraction, deduplication under concurrency, depth limits, the concurrency limit (counting simultaneous requests on the server), error handling, and cancellation. Done when: sbt test passes reliably.
Ready to build this?
Get a GitHub repo and start building. Your AI reviewer checks each step as you go.
Tech stack