coldwa.st_

Haskell · Cabal · build performance

Parallelising cabal-install: How Haskell Builds Use Your Cores

The 2011 Google Summer of Code work on this topic was a landmark for the domain · this is an updated, original community guide for 2026 · Coldwast Programming Guides

Parallel build lanes showing independent Haskell packages compiling concurrently under cabal build -j
Independent packages in the install plan build concurrently — the core idea behind today's -j builds.
Community-maintained, updated rewrite. The original 2011 Google Summer of Code project "Parallelising cabal-install" was carried out by the domain's previous owner; this guide is new, original writing about the topic and does not reproduce or claim authorship of that work. It explains the idea and brings it up to the 2026 toolchain.

In 2011, building a Haskell project with a large dependency tree was a sequential slog: cabal install compiled one package, then the next, then the next — leaving most of a multicore machine idle. A Google Summer of Code project that year tackled exactly this: teach cabal-install to build independent packages in parallel. That idea is now baked into every Haskell build. Here is how it works, and how to get the most out of it today.

The key insight: a build is a DAG

An install plan is a directed acyclic graph of packages. If aeson and vector both depend only on base, they don't depend on each other — so there is no reason to build them one after the other. You can compile every package whose dependencies are already built at the same time, bounded only by how many cores you want to use.

Concretely the builder:

  1. Topologically sorts the dependency graph.
  2. Maintains a "ready" set of packages whose dependencies are all built.
  3. Runs up to N builds from that set concurrently; as each finishes, any newly-unblocked package joins the ready set.

On a wide dependency graph and an 8- or 16-core machine, that turns a multi-minute cold build into something dramatically shorter.

Using it today (2026)

Package-level parallelism is on by default and tunable with -j:

$ cabal build -j         # use all available cores
$ cabal build -j4        # cap at 4 concurrent package builds

Or set it persistently in cabal.project (or ~/.cabal/config):

# cabal.project
jobs: 4

This is safe today in a way it wasn't originally, thanks to nix-style local builds (see Cabal 2.0): every package-version-flags combination builds into a content-addressed global store, so two parallel builds can never clobber each other's output. Isolation makes aggressive parallelism risk-free.

Two layers of parallelism

There are actually two independent axes, and understanding both is how you avoid oversubscribing your CPU:

The trap is multiplying them: 8 parallel packages × 8 parallel modules each = up to 64 concurrent GHC threads on an 8-core box, which thrashes rather than helps.

The modern fix: a build semaphore

Recent GHC and Cabal solve the oversubscription problem with a shared job semaphore. Instead of each package independently spawning N module jobs, Cabal hands GHC a single semaphore so that total concurrency — across all packages and all modules — stays bounded by one number:

# cabal.project
jobs: 8
package *
  ghc-options: -jsem    # coordinate module + package parallelism
                        # via a shared semaphore

With a semaphore, "8 jobs" means at most 8 GHC compilation tasks running at once in total, whether they come from one big package's modules or eight small packages — exactly the right behaviour on an 8-core machine.

Practical tuning

The takeaway

The 2011 GSoC work answered a question that still matters: a build is a dependency graph, and independent nodes should compile concurrently. Fifteen years later that idea is the default, made safe by content-addressed storage and refined by build semaphores. If your Haskell builds still feel sequential, you are almost certainly leaving cores on the table — start with cabal build -j.


Related: Cabal 2.0 and nix-style builds · Cabal sandboxes and what replaced them · all guides