Skip to Content
Course content

75: Fibers and Concurrency in Cats Effect

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're running a high-end restaurant kitchen. You only have three world-class chefs (these are your OS threads). Now, imagine you have 500 orders coming in at once. If each chef picked one order and stood there staring at the toaster for three minutes waiting for the sourdough to brown, your kitchen would grind to a halt. You'd need 500 chefs to handle 500 orders, which is an expensive and physically impossible way to run a business.

Instead, the chefs use "orders slips" (Fibers). A chef starts the toast, sets a timer, and immediately pivots to chopping parsley for another dish. They aren't "finished" with the toast, but they aren't "blocked" by it either. When the timer dings, any available chef can jump back in and plate the toast. The "work" (the fiber) is decoupled from the "worker" (the thread).

In Cats Effect, a Fiber is essentially that order slip. It's a lightweight "virtual thread" managed by the Cats Effect runtime rather than the JVM or the Operating System. This is why you can spawn 100,000 fibers on a laptop without crashing it, whereas 100,000 OS threads would trigger an OutOfMemoryError almost instantly.

Turning IO into a Fiber

Usually, when you run an IO, you're running it synchronously within the current execution context. But if you want to kick off a process in the background and keep moving, you use .start. This transforms your IO[A] into an IO[Fiber[A]].

import cats.effect._

val fetchUserStatus = IO.sleep(1.second) >> IO.pure("User is Online")
val fetchSystemLogs = IO.sleep(2.seconds) >> IO.pure("Logs are clear")

val program = for {
  fiber1 <- fetchUserStatus.start // This starts immediately in the background
  fiber2 <- fetchSystemLogs.start // This also starts immediately
  _      <- IO.println("I'm doing other work while the services respond...")
  status <- fiber1.join            // Now we wait for the result
  logs   <- fiber2.join            // Wait for the second result
} yield s"Result: $status, $logs"

I want you to notice that .start doesn't execute the effect in the way unsafeRun` (or your IOApp) does; it schedules it. The Fiber handle is your way of keeping track of that background work. If you lose that handle without joining it, the work still happens, but you've lost the ability to get the result or stop it.

Cleaning up with Cancellation

One of the most powerful parts of fibers is that they are cancellable. Back in our kitchen, if a customer cancels their order while the toast is still browning, the chef doesn't finish the toast and then throw it away. They just stop.

In Scala, if you have a Fiber, you can call .cancel. This sends a signal to the fiber to stop immediately. This is crucial for things like request timeouts or when a user navigates away from a page in a UI, making the background API call irrelevant.

val longTask = IO.sleep(10.seconds) >> IO.println("Task Finished!")

val cancellationDemo = for {
  fiber <- longTask.start
  _      <- IO.sleep(1.second)
  _      <- IO.println("Too slow! Cancelling...")
  _      <- fiber.cancel
} yield ()

I should mention: for cancellation to work effectively, the code inside the fiber must be "cancellation-aware." Luckily, almost everything in the Cats Effect ecosystem (like IO.sleep or Evaluator) is designed to check for cancellation signals automatically.

The Magic of Parallel Traversal

In production, you'll rarely manage Fiber objects manually. It's a bit too low-level for most business logic. Instead, we use parSequence or parTraverse. These utilities handle the spawning, joining, and—most importantly—the error propagation for you.

If you have a list of URLs to fetch, you don't want to loop through them and .start each one. You want them to run in parallel and give you back a list of results once they are all done.

import cats.implicits._
import cats.effect.IO

val endpoints = List("api/users", "api/posts", "api/comments")
val fetch = (url: String) => IO.println(s"Fetching $url") >> IO.pure(s"Data from $url")

val parallelFetch = endpoints.parTraverse(fetch) 
// This spawns a fiber for every element in the list and joins them all.

Here is a pro tip: be careful with parTraverse on massive lists. If you have 10,000 URLs, you'll spawn 10,000 fibers. While the runtime can handle the fibers, the target API probably can't handle 10,000 concurrent requests. In those cases, you'd want to look into parallel` (from the cats-effect concurrency utilities) to limit the concurrency factor.




📋 Practical Task

Build a Concurrent Microservice Health Checker

You need to build a health checker that monitors three simulated microservices. Each service takes a different amount of time to respond. Your program must:

  • Simulate three services: AuthService (slow, 3s), PaymentService (medium, 1s), and InventoryService (fast, 0.5s).
  • Start all three checks concurrently using .start.
  • Implement a "Global Timeout": If the AuthService doesn't return within 2 seconds, cancel its fiber and mark it as "Timed Out" in the final report.
  • Join the remaining fibers and print a final summary of which services are healthy and which timed out.

Constraint: Do not use parTraverse for this specific exercise; I want you to practice manual Fiber management and .cancel to understand the underlying mechanics.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.