TL;DR:

  • Turborepo caches build outputs and task results so unchanged packages don’t rebuild — dramatically reducing build times in both local dev and CI
  • It runs tasks in parallel across packages with dependency-aware ordering, replacing slow sequential package-by-package builds
  • Remote caching shares the cache across all team members and CI runs, so a build that ran on one machine won’t need to run again on another

If you maintain a JavaScript or TypeScript monorepo with multiple packages, you’ve probably noticed that builds get slower as the repo grows. Type-checking all packages, running tests, building libraries — each task multiplies by the number of packages in the repo. Turborepo, acquired by Vercel in 2021 and now part of the standard JavaScript tooling stack, solves this by making builds content-addressed: if the inputs haven’t changed, reuse the output.

The Core Idea: Content-Addressed Task Outputs

Turborepo hashes the inputs to each task — the source files, environment variables, and dependency outputs. If you run turbo build and nothing in a package has changed since the last successful build, Turborepo restores the output from cache rather than running the build again. The task completes in milliseconds.

This isn’t language-specific: Turborepo works with any task runner (npm scripts, shell commands) and any build tool underneath it. If you can define what files are inputs and what files are outputs, Turborepo can cache it.

Setup

Add Turborepo to an existing monorepo:

# Using pnpm (recommended)
pnpm add turborepo --save-dev -w

# Or npx (no install needed)
npx create-turbo@latest

Add a turbo.json at the monorepo root:

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

The dependsOn: ["^build"] syntax means “run the build task in all dependencies of this package first”. The caret (^) means upstream packages. This tells Turborepo to build packages in the correct order based on the dependency graph in package.json.

Once configured, replace your existing scripts:

# Before: builds packages one at a time
npm run build --workspaces

# After: builds in parallel, in the right order, with caching
turbo build

Parallel Execution

Without Turborepo, most monorepo setups either run tasks sequentially (slow) or run all tasks at once regardless of dependencies (breaks when package A needs package B to be built first).

Turborepo builds a dependency graph and runs tasks concurrently wherever there are no dependencies between them. If you have 10 packages and only 3 depend on each other, Turborepo can run 7 of them in parallel while building the dependency chain for the other 3.

On a 4-core machine this is often the bigger win than caching for the first build — you’re running tasks in 4-8 parallel processes instead of sequentially.

Remote Caching

Local caching helps within one machine, but each CI runner starts fresh. Without remote caching, every CI run rebuilds everything from scratch even if only one file changed.

Remote caching shares the Turborepo cache across all machines and CI runners:

# Log in to Vercel (free for personal use)
npx turbo login

# Link your repo to a remote cache
npx turbo link

After this, when CI runs turbo build, it fetches cached outputs from the remote cache for unchanged packages. A PR that touches only the ui package rebuilds only ui and the packages that depend on it. Everything else hits the cache.

For organisations that can’t use Vercel’s cache, Turborepo supports self-hosted remote caches. ducktape and turborepo-remote-cache are open-source implementations that work with any S3-compatible storage.

Filtering Tasks to Affected Packages

In a CI pipeline, you often want to run tasks only for packages that changed in a given PR:

# Only run build and test for packages changed since branching from main
turbo build test --filter=...[origin/main]

The [origin/main] filter means “packages that have changes compared to the main branch”. Combined with remote caching, this means: only packages with actual changes get rebuilt, and cached outputs are reused for everything else.

For a monorepo with 20 packages, a PR that touches one package might rebuild and test 3 packages (the changed one plus dependents) rather than all 20.

Practical CI Configuration

# GitHub Actions
- name: Build and test
  run: turbo build test lint typecheck
  env:
    TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
    TURBO_TEAM: ${{ vars.TURBO_TEAM }}

The TURBO_TOKEN and TURBO_TEAM environment variables connect the CI runner to the remote cache. Once set, Turborepo handles cache reads and writes automatically.

What Turborepo Doesn’t Do

Turborepo handles task scheduling, caching, and parallelisation. It doesn’t manage package versions or publishing (that’s changesets or nx release), and it doesn’t provide a module federation or shared dependency management solution — that’s still your package manager’s job.

For teams that need more — cross-language monorepos, build system integration with Bazel or Gradle, or complex dependency analysis beyond JavaScript packages — Nx is a more capable (and more complex) alternative. Turborepo is the right tool when you want fast builds in a JavaScript monorepo without taking on a framework that requires learning a new project structure.

The Turborepo documentation is at turbo.build/repo/docs, with examples for Next.js, plain TypeScript packages, and mixed frontend/backend monorepos.