Speeding Up CI with Turborepo’s Pipeline and Content‑Hash Caching
Learn how Turborepo’s task pipeline and content‑hash cache reduce redundant work in JavaScript monorepos, and see a concrete configuration you can try today.
18 Mar 2026, 22:07 UTC

The problem: duplicated work in monorepo CI
When a monorepo grows, CI pipelines often repeat the same build, test, or lint steps for packages that haven’t changed. Even with workspace‑aware package managers, many CI configurations still run every task on every commit, wasting minutes (or hours) and increasing costs.
Thesis: Turborepo’s pipeline plus content‑hash caching lets you run only the work that actually changed, and share those results across machines.
By declaring task dependencies in turbo.json and letting Turborepo hash the exact inputs, you get two benefits: (1) tasks are scheduled in topological order and run in parallel where possible, and (2) if the hash of a task’s inputs matches a previous run, Turborepo restores the outputs from its cache instead of re‑executing the task.
How the pipeline works
In turbo.json you list each task (e.g., build, test) and its dependencies using the dependsOn field. Turborepo builds a directed acyclic graph of all tasks across all packages and executes them in topological order. Independent branches of the graph run concurrently, which makes full use of available CI workers.
Example snippet for a simple monorepo with two packages, web and api:
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build", "^test"],
"inputs": ["src/**", "test/**", "jest.config.js"]
},
"lint": {
"dependsOn": ["^lint"],
"inputs": ["src/**", "eslint.config.js"]
}
}
}
The ^ prefix means “this task in all dependent packages”. When you run turbo run build, Turborepo first builds any package that web or api depends on, then builds the packages themselves, all in parallel where the graph allows.
Content‑hash caching: what gets hashed
For each task, Turborepo computes a hash over the inputs you declare:
- Source files matched by
inputsglobs - Environment variables listed in
env - The lockfile (
package-lock.json,yarn.lock, orpnpm-lock.yaml) - The task configuration itself (the relevant section of
turbo.json)
If the hash matches a previous entry in the local cache (node_modules/.cache/turborepo), Turborepo skips the command, replays the stored stdout/stderr, and restores the declared outputs. The second run therefore reports FULL TURBO and finishes in a fraction of a second.
Remote caching: sharing the cache across machines
Local caching helps when you rebuild the same commit on your laptop, but the biggest win in CI comes from reusing artifacts produced by a teammate’s machine or a previous pipeline run. Turborepo can push and pull cache entries to a remote server that implements its cache API (Vercel’s hosted remote cache or a self‑hosted compatible service).
To enable remote caching, add a remoteCache section:
{
"remoteCache": {
"endpoint": "https://cache.example.com",
"token": "${TURBO_TOKEN}"
}
}
When CI runs turbo run build, it first attempts to restore any matching cache entries from the remote; after the run, it pushes new entries back. This turns the CI pipeline into a “pull‑then‑push” model, often cutting total pipeline time by 50‑80 % for typical monorepos.
Worked example: verifying a cache hit
Assume you have cloned a repo with the turbo.json above and have turbo installed (npm i -D turborepo). Follow these steps to see the cache in action:
- Run the build the first time:
Observe the output; each package’s build step will show execution time (e.g.,# In the repo root turbo run build --summarize120ms). - Run the same command again without changing any source files:
The summary should now showturbo run build --summarizeFULL TURBOfor each package and the total time drop to near‑zero (often<10ms). - Change a single source file in
web/src/App.tsxand run the build once more:
Only theturbo run build --summarizewebbuild task (and any downstream tasks that depend on it, liketestorlint) should execute; theapipackage’s tasks will report cache hits.
These steps confirm that Turborepo is correctly hashing inputs and reusing outputs.
Limitation: honest input declaration is critical
The cache is only correct if every file or environment variable that a task actually reads is listed in inputs or env. Forgetting to declare a shared configuration file (e.g., a tsconfig.base.json that lives outside the package) can cause Turborepo to reuse a stale build, leading to subtle bugs that are hard to trace.
Practical check: after a change you expect to affect a task, run turbo run <task> --dry-run. The dry‑run output lists the hash inputs Turborepo considered; verify that any file you changed appears in that list. If it does not, add it to inputs and re‑test.
Actionable closing
If your monorepo’s CI spends minutes rebuilding unchanged code, start by adding a turbo.json that declares your existing npm scripts and their dependencies. Enable local caching first; you’ll see immediate speed‑ups on repeated runs. Then, configure a remote cache (Vercel’s hosted option is the simplest to try) and measure the CI duration before and after. Keep a close eye on the inputs and env lists—treat them as part of your contract with the cache. With those guards in place, Turborepo’s pipeline and content‑hash cache become a reliable lever for faster, cheaper CI.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.