← Back to articles
Tooling TurborepoMonorepoDevOpsTypeScript

Turborepo: Monorepo Builds That Only Run What Changed

· 4 min read

A monorepo lets you share code across apps and packages, but the build gets slower with every package you add. A root build script re-runs everything, every time, even for code that has not changed since the last commit, and a full CI pass ends up rebuilding and retesting the entire tree to ship a one-line fix. Turborepo attacks this by treating your scripts as a task graph instead of a flat list. It understands which packages depend on which, runs independent tasks in parallel, and replays anything it has already built from cache in milliseconds.

The task graph

Turborepo reads a turbo.json at the root where each script becomes a task with declared dependencies. The ^build syntax means “build every internal dependency first,” which is enough for Turborepo to compute a correct execution order across the whole workspace.

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

You never list packages by hand. Turborepo derives the graph from your workspace manifests, so adding a package or a new dependency edge reshapes the build order automatically.

Content-aware caching

Each task run is hashed from everything that can affect its output: source files, dependencies, environment variables and the task config itself. If that hash has been seen before, Turborepo skips the work entirely and restores the previous outputs and logs from cache.

turbo run build
# first run:  10 packages built in 48s
# second run: 10 packages, FULL TURBO, 120ms (all replayed from cache)

The outputs field tells Turborepo which files to capture so a cache hit can restore them. Because the hash is content-based, changing one package invalidates only that package and the tasks downstream of it, never the whole tree.

Running and filtering tasks

Most of the time you run a task across the whole repo, but --filter narrows execution to a package and, optionally, everything it depends on or that depends on it. This is what keeps local iteration and CI fast when you only touched one corner of the monorepo.

turbo run lint test --filter=web
turbo run build --filter=...@acme/ui
turbo run test --filter=[origin/main]

The last form scopes work to packages changed since a Git ref, so a pull request only ever builds and tests what its diff actually affects. Independent tasks in the resulting set still run in parallel across your cores.

Remote caching

Local caching only helps the machine that produced it. Remote caching pushes those hashed artifacts to a shared store, so a cache filled by one developer or one CI job is a cache hit for everyone else.

turbo login
turbo link
# CI and teammates now share the same artifact cache

The payoff is sharpest in CI: if a package was already built for an earlier commit with identical inputs, the pipeline downloads the result instead of rebuilding it. A green build on an unrelated change can drop from minutes to seconds.

Pruning for Docker

Copying an entire monorepo into a Docker image wrecks layer caching, because any change anywhere busts the dependency-install layer. turbo prune generates a partial monorepo containing only a target app and the packages it actually needs, with a trimmed lockfile.

turbo prune @acme/web --docker
# creates out/ with a minimal, installable subset of the repo

You copy the pruned lockfile and manifests first, install, then copy the source. Docker caches the install layer until real dependencies change, which is exactly the behavior a monorepo otherwise breaks.

Where it fits

Turborepo is not a bundler and not a package manager. It sits on top of your existing pnpm, npm or Yarn workspaces and orchestrates the scripts you already have, which means adoption is incremental: you drop in a turbo.json, point your scripts at turbo run, and nothing about how each package builds has to change. It coordinates the work rather than replacing the tools that do it.

Conclusion

Turborepo pays for itself the moment a monorepo grows past a couple of packages, because it stops paying to rebuild code that did not change. The task graph gives you correct ordering for free, content-based caching turns most builds into instant replays, and remote caching extends that speedup across your whole team and CI. It is a thin layer that changes nothing about your individual tools and everything about how fast they run together. For any serious TypeScript monorepo, that trade is hard to pass up.