coldwa.st
All guidesProgrammingWebDataToolsDatabasesHaskellConceptsCabal & buildsToolchainCompilerPerformanceEditor & HLS

Haskell · Cabal · build performance

Parallelising cabal-install: How Haskell Builds Use Your Cores

By ColdwastUpdated Jun 13, 20266 min read#haskell#cabal#performance
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)

Lines of source code on a dark screen
Lines of source code on a dark screen

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:

  • Package-level (cabal build -j): build several independent packages at once. This is the descendant of the 2011 work.
  • Module-level (GHC's own -j): within a single package, GHC can compile independent modules in parallel. You enable it via GHC options, e.g. ghc-options: -j.

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

  • Cold CI builds: cabal build -j (all cores) is almost always the win - the dependency graph is wide and CI machines are otherwise idle.
  • Interactive dev: consider leaving a core or two free (-j$(($(nproc)-2))) so your editor/LSP stays responsive during a rebuild.
  • Memory, not cores, is often the limit. GHC can use a lot of RAM per compilation; if a parallel build starts swapping, lower -j. Cores are cheap, RAM pressure is what actually stalls big Haskell builds.
  • Measure once: time a clean build at -j2, -j4, -j on your own hardware and dependency set - the sweet spot depends on both.

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