# Cats Effect: the runtime that makes effect types mean something on the JVM

> Typelevel's IO is not a monad tutorial. It is a thread pool with an interruption story, a resource story, and a compatibility promise the rest of the ecosystem builds on.

**typelevel/cats-effect** — The pure asynchronous runtime for Scala

- Repository: https://github.com/typelevel/cats-effect
- Website: https://typelevel.org/cats-effect/
- Stars: 2,242 · Forks: 581
- Language: Scala
- License: Apache-2.0
- Published: 2026-10-07 · Updated: 2026-10-07 · Language: en
- Canonical page: https://hysenlabs.com/projects/typelevel-cats-effect

## IO is a description, and the runtime runs it

The README's opening paragraph describes Cats Effect as a high-performance, asynchronous, composable framework for building real-world applications in a purely functional style within the Typelevel ecosystem. The concrete tool it names is the `IO` monad, for capturing and controlling actions, called effects, that the program wishes to perform within a resource-safe, typed context.

The framing matters more than it first appears. An `IO[A]` is not a value you await, it is a description of an action that has not run yet. Building it is pure and cheap. Running it is what allocates, blocks, schedules and can be interrupted. That split is the whole point of the design, and it is why the same library can be used for an action that returns within microseconds and one that runs indefinitely.

```scala
import cats.effect._

object Main extends IOApp.Simple {
  val run = IO.println("Hello, World!")
}
```

That is the Hello World, and the presence of `IOApp.Simple` rather than a `def main` is already a design decision. When you need arguments and an exit code you widen it:

```scala
object Main extends IOApp {
  def run(args: List[String]): IO[ExitCode] =
    if (args.headOption.map(_ == "--do-it").getOrElse(false))
      IO.println("I did it!").as(ExitCode.Success)
    else
      IO.println("Didn't do it").as(ExitCode(-1))
}
```

The runtime owns the entry point, which means it owns the thread pool, the shutdown sequence and what happens to fibers still running when `run` returns.

## Five rules, and every one of them is about thread discipline

The README calls these the Five Simple Rules and frames them as the condition for getting strong guarantees. They are worth reading as a single idea rather than five: on the JVM, the way to get parallelism without thread starvation is to keep blocking operations off the compute pool, and every rule is a consequence of that.

Wrap all side-effects in `delay`, `async`, `blocking`, or `interruptible`/`interruptibleMany`, with the pro tip to keep `delay` blocks small because two delays with a `flatMap` beats one big delay. Use `bracket` or `Resource` for anything which must be closed. Never hard-block a thread outside `blocking` or `interruptible`/`interruptibleMany`. Use `IOApp` instead of writing your own `def main`. Never call anything that has the word unsafe in the name.

That last one is a naming convention doing policy work. If a method is called unsafe, the runtime is telling you it will not be interrupted and will hold a thread for its duration, which is precisely what the other rules exist to avoid.

The list of things the README claims follow from compliance is worth reading for calibration: extremely high performance, elastic and scalable applications, proven backpressure mechanisms under extreme load in real deployments, reliable resource safety in all cases, aggressive interruption of unnecessary work such as timeouts without extra implementation effort, concurrency mechanisms that get faster under high contention, and composable and modular architecture. The interruption claim is the one most users notice first, because a timeout on a long-running computation actually cancels the work rather than merely ignoring its result.

## Three dependencies for three different kinds of consumer

The Getting Started section is unusually good at telling you which artifact to depend on, because the right answer changes with what you are building. The README names a Wired version and a Tired one, marking 2.5.5 as end of life, and defaults to the core artifact.

```scala
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.7.1"
```

All current releases are published for Scala 2.12, 2.13, 3.2, and Scala.js 1.13. The core dependency brings in the entirety of Cats Effect and is described as most likely what you want.

If you implement datatypes, in the README's example someone like Monix, the guidance is to depend only on kernel, the typeclasses, in compile scope and laws in test scope. That keeps the abstract algebra separate from any concrete runtime, which is what stops your library from forcing an effect type on its users.

```scala
libraryDependencies ++= Seq(
  "org.typelevel" %% "cats-effect-kernel" % "3.7.1",
  "org.typelevel" %% "cats-effect-laws"   % "3.7.1" % Test)
```

Middleware, in the README's example Fs2, wants std, which gives access to `Queue`, `Semaphore` and more without a hard dependency on IO outside of tests. Testkit and kernel-testkit hold `TestContext` and generators for IO. Getting this wrong is the most common way people end up with a version conflict, because a library that depends on core cannot be used by someone who only wanted the typeclasses.

## The compatibility promise is the load-bearing feature

The README states it precisely: Cats Effect provides backward binary compatibility within the 2.x and 3.x version lines, and both forward and backward compatibility within any major/minor line. The worked example is that a project depending on 2.2.1 can use libraries compiled against 2.0.0 or 2.2.3, but not against 2.3.0 or higher.

This is analogous to the versioning scheme used by Cats itself and by Scala.js, and the README says so. For an ecosystem where a dozen libraries are compiled independently against a runtime's typeclasses, that guarantee is what allows them to be combined. Without it, every runtime upgrade becomes a coordinated release across every library in the graph, and in practice it does not happen.

There is a migration guide linked for updating from 1.x or 2.x, which tells you the boundary is real rather than theoretical. The version numbers in the repository confirm the current line: three releases are recorded, in March 2026 and twice in August 2026, with the README pinning 3.7.1 as wired. The repository was last pushed on 2026-09-28 with 232 open issues against 2238 stars and 578 forks, Apache-2.0 licensed, not archived.

## What the repository is actually made of

The tree shows a multi-module build in the sbt style, which matches the dependency structure above. Each published artifact has its own directory: `kernel/` for the typeclasses, `laws/` for the property laws they must satisfy, `std/` for Queue and Semaphore, `core/` for IO itself, `testkit/` and `kernel-testkit/` for testing support.

Around those sit the parts of a project that are about running rather than about code. `tests/` and `ioapp-tests/` are separate suites, which makes sense when the thing under test includes process exit behaviour. `benchmarks/` is its own directory, matching the README's performance argument. `example/` and `graalvm-example/` show a normal app and a native-image build, and `docs/` plus `site-docs/` separate developer documentation from the published site.

The infrastructure files are also telling. `flake.nix` and `flake.lock` provide a Nix-based development environment, `.jvmopts` pins JVM options, `.scalafmt.conf` and `.scalafix.conf` handle formatting and lint rewriting, `.mergify.yml` automates dependency merge queues, and `RELEASING.md` alongside `CONTRIBUTING.md` document how versions are cut. `NOTICE.txt` and `LICENSE.txt` are the Apache-2.0 pair.

One oddity: there is a `package.json` containing only `source-map-support` as a devDependency. In a Scala project that is a small concession to Node-based tooling around stack traces, and it is the kind of detail that tells you the project leans on developer experience tooling rather than shipping a bare library.

## Reading the performance claim carefully

The README's performance section makes a specific argument rather than quoting numbers. Most functional and async frameworks tout performance on synthetic microbenchmarks, measuring how many `flatMap`s they can evaluate per microsecond. The README's counter is that most programs are not just a bunch of `flatMap`s, and the real bottlenecks are contention scaling under high load, memory and other resource management, backpressure, and page faults.

The evidence offered is a single bar chart in `images/contention.png`, described as comparing a fixed thread pool against Cats Effect 3 with the latter substantially taller. That is a screenshot of a chart rather than a reproducible benchmark in the repository, so a reader who wants the numbers has to look at `benchmarks/` or the Typelevel site. Worth noting plainly: the claim that functional applications can exceed the performance and elasticity of the same applications written imperatively is the project's own assessment.

The README also lists a set of downstream projects in the abstract rather than by name: streaming frameworks, JDBC database layers, HTTP servers and clients, and asynchronous clients for systems like Redis and MongoDB. That list is the ecosystem, and it is the practical argument for adopting the typeclasses even if you never run IO yourself, since anything you build that those libraries consume has to be compatible with them.

## Conclusion

Cats Effect is worth understanding even if you never write the IO type by hand, because its typeclasses are the contract that Fs2, http4s, the JDBC layers and the Redis clients in the Typelevel ecosystem are all compiled against, and because the binary compatibility promise in the README is what lets those libraries coexist across patch releases. The five rules are the part to internalise: wrap every side effect in `delay`, `async`, `blocking` or `interruptible`, use `bracket` or `Resource` for anything that closes, never hard-block a thread outside those builders, use `IOApp` instead of your own `main`, and never call anything with unsafe in the name. Follow those and you get interruption, backpressure and resource safety from the runtime rather than from your own bookkeeping. Start by depending on kernel and laws only, which is what the README recommends for datatype implementers, and add the full core dependency when you actually need IO.

## FAQ

### What is Cats Effect in Scala?

It is an asynchronous, composable framework for building real-world applications in a purely functional style within the Typelevel ecosystem, and its central tool is the `IO` monad. An `IO[A]` describes an action rather than performing it, so building effects is pure and cheap while running them is what allocates threads, blocks and can be interrupted.

### Should I depend on cats-effect, cats-effect-kernel, or cats-effect-std?

The core `cats-effect` dependency brings in the whole thing and is what an application normally wants. Datatype implementers should depend only on `cats-effect-kernel` in compile scope with `cats-effect-laws` in test scope, which keeps the typeclasses separate from any concrete runtime. Middleware libraries want `cats-effect-std` for Queue and Semaphore without a hard dependency on IO.

### What do the five rules of Cats Effect actually ask me to do?

Wrap every side effect in `delay`, `async`, `blocking` or `interruptible`, use `bracket` or `Resource` for anything that must be closed, never hard-block a thread outside those builders, use `IOApp` rather than your own `def main`, and never call anything with unsafe in its name. All five come back to keeping blocking work off the compute pool so the scheduler can use its threads for real work.

### Can I upgrade Cats Effect without upgrading every library that depends on it?

Within a version line, yes. The README states backward binary compatibility within the 2.x and 3.x lines and both forward and backward compatibility within any major/minor line, so a project on 2.2.1 works with libraries compiled against 2.0.0 or 2.2.3 but not 2.3.0 or higher. Crossing a major line still means the migration guide.

### Which Scala versions does Cats Effect support?

All current releases are published for Scala 2.12, 2.13, 3.2, and Scala.js 1.13. The README also tracks two release lines, marking 3.7.1 as wired and 2.5.5 as end of life.

## Sources

- [License: Apache-2.0](https://github.com/typelevel/cats-effect/blob/series/3.x/LICENSE)
- [Project website](https://typelevel.org/cats-effect/)
- [README](https://github.com/typelevel/cats-effect/blob/series/3.x/README.md)
- [Releases](https://github.com/typelevel/cats-effect/releases)
- [typelevel/cats-effect on GitHub](https://github.com/typelevel/cats-effect)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/typelevel-cats-effect
