How We Cut Monorepo CI Time by Over 60% with Turborepo Caching Strategy: Real-World turbo.json and Remote Cache Configuration
You've probably experienced the frustration of pushing a single PR only to watch CI run for over 15 minutes, or waiting several minutes for a full local build when you only touched one package. This is an almost unavoidable problem when operating a monorepo (a structure where multiple packages are managed together in a single repository). I spent a long time wondering, "Is something misconfigured?" when I first encountered it.
Turborepo solves this problem through content-based hashing and dependency graph execution. But simply adopting it only gets you half the benefit. How you configure turbo.json and whether you connect Remote Cache will completely change the build experience for your entire team. Looking at a case published on johal.in, they saved $11,800 annually in CI costs after migrating to Next.js 15 + Turborepo 2.0. By the end of this article, you'll be able to measure cache hit rates directly using three key turbo.json settings and connect Remote Cache to your CI pipeline. These are settings that can reduce CI time by 60–80% for single-package PRs in a 15-package monorepo.
Core Concepts
How Turborepo Makes Builds Faster
The hashing concept was confusing at first, but the core idea is simple: if file contents haven't changed, there's no reason to rebuild. Turborepo combines source files, package.json dependencies, environment variables, and build configuration to create a unique fingerprint per task. If this hash already exists in the cache, it restores results in milliseconds without an actual build.
Two more things are layered on top of this. By specifying task execution order with the dependsOn field in turbo.json, it automatically understands inter-package dependency relationships, schedules builds in optimal order, and runs tasks with no dependencies in parallel using all available CPU cores. Instead of running lint for 10 packages sequentially, it processes them all in parallel simultaneously.
Understanding the turbo.json Structure
Starting with Turborepo 2.0, the pipeline key was removed and consolidated into tasks. This is exactly why configurations copied from older setups stop working.
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "package.json"],
"outputs": [".next/**", "dist/**", "!.next/cache/**"],
"env": ["NODE_ENV", "API_URL"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tests/**"],
"outputs": ["coverage/**"]
},
"lint": {
"dependsOn": [],
"inputs": ["src/**", ".eslintrc*"]
},
"deploy": {
"dependsOn": ["build", "test"],
"cache": false
}
}
}In dependsOn, ^build means "the build of upstream packages that this package depends on must complete first." If app uses the ui package, ^build guarantees that ui's build runs first. It's easy to confuse the direction — this refers to upstream (packages this package uses), not downstream (packages that use this package).
"dependsOn": [] on the lint task means "run immediately without waiting for anything." It runs independently in parallel with no relation to other tasks.
Here's a summary of how each field affects caching:
| Field | Role | Cache Impact |
|---|---|---|
dependsOn |
Determines task execution order | Whether prerequisite tasks completed is reflected in the hash |
inputs |
Specifies files to include in hash calculation | Cache is only invalidated when specified files change |
outputs |
Paths of artifacts to store in cache | File caching doesn't work if omitted |
env |
Environment variables to include in hash | Cache is invalidated when values change |
cache |
Enables/disables caching | Always re-runs when set to false |
One thing worth noting: it's recommended not to put .env files in inputs. They're usually in .gitignore, and if team members have different contents, hash mismatches will actually lower your cache hit rate. Environment variables should be managed through the env field.
Practical Application
Example 1: Improving Cache Hit Rate with outputs Configuration
Honestly, when I first adopted Turborepo, "why isn't the cache working?" was caused by missing outputs. Turborepo only saves and restores paths explicitly listed in outputs to/from the cache. If it's an empty array or omitted, file caching doesn't work at all and a full build happens every time.
{
"tasks": {
"build": {
"outputs": [
".next/**",
"!.next/cache/**",
"dist/**",
"storybook-static/**"
]
}
}
}You can exclude paths using the ! prefix, like !.next/cache/**. For Next.js, .next/cache is Next.js's own internal cache, so it must be excluded from Turborepo's cache to avoid conflicts. Including it just bloats the cache size unnecessarily.
There's also a way to verify that cache hits are actually working after configuration.
# Output detailed report of which tasks are cache hits
turbo run build --summarize
# Check execution plan only without actually running
turbo run build --dry-runUsing the --summarize option lets you see cache hit status, execution time, and hash information per task, so you can immediately verify the effect each time you change a setting.
Here are the recommended outputs settings by framework:
| Framework | Recommended outputs | Excluded Paths |
|---|---|---|
| Next.js | .next/** |
!.next/cache/** |
| Vite | dist/** |
— |
| Storybook | storybook-static/** |
— |
| NestJS | dist/** |
— |
| Jest | coverage/** |
— |
Example 2: Connecting Vercel Remote Cache (Fastest Method)
Local cache is only stored on your machine. Teammates can't share that cache even when building the same code. Remote Cache makes this cache storage shared across the entire team.
When developer A builds the feat/payment branch, the results are uploaded remotely. When developer B checks out the same branch and builds, it detects the same hash and downloads the cached result without an actual build. The same applies to CI pipelines. The larger the team and the higher the CI run frequency, the greater the effect.
If you're already using Vercel, setup is really simple.
# Log in with your Vercel account
turbo login
# Connect remote cache to the project
turbo linkRunning turbo link automatically creates .turbo/config.json. The contents look like this:
{
"teamid": "team_xxxxx",
"apiurl": "https://vercel.com"
}For CI pipelines (GitHub Actions), configure using environment variables. This is cleaner for CI environments since you don't need to commit .turbo/config.json.
# .github/workflows/ci.yml
jobs:
build:
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_REMOTE_ONLY: true
steps:
- uses: actions/checkout@v4
- run: pnpm install
- run: pnpm turbo build test lintTURBO_TOKEN can be generated from the Vercel dashboard → Account Settings → Tokens, and TURBO_TEAM is your team slug. Just register them in GitHub Secrets. TURBO_REMOTE_ONLY: true is an option that skips local cache and uses only remote cache in CI environments.
Example 3: Self-Hosted Remote Cache (Running Without Vercel)
If you want to upload cache directly to S3 or GCS without a Vercel account, you can use the open-source ducktors/turborepo-remote-cache. The barrier to entry is low since it runs with a single Docker container.
# Run cache server with Docker (S3 storage example)
docker run -d \
-e STORAGE_PROVIDER=s3 \
-e AWS_ACCESS_KEY_ID=xxx \
-e AWS_SECRET_ACCESS_KEY=xxx \
-e S3_BUCKET=my-turbo-cache \
-p 3000:3000 \
ducktors/turborepo-remote-cacheAfter starting the server, you only need to change the apiurl in .turbo/config.json.
{
"teamid": "my-team",
"apiurl": "http://my-cache-server:3000"
}A variety of storage backends are supported, so you can choose based on your infrastructure environment:
| Storage | Environment Variable | Notes |
|---|---|---|
| AWS S3 | STORAGE_PROVIDER=s3 |
Most common |
| GCS | STORAGE_PROVIDER=gcs |
GCP environments |
| Cloudflare R2 | STORAGE_PROVIDER=s3 + R2 endpoint |
Uses S3-compatible API |
| Azure Blob | STORAGE_PROVIDER=azureBlob |
Azure environments |
Pros and Cons Analysis
Advantages
| Item | Details |
|---|---|
| Build Speed | 2–3x faster with local cache alone; 10–15x faster possible when combined with remote cache |
| Team Collaboration | Cache sharing between teammates enables immediate builds after first checkout |
| CI Cost | 60–80% reduction in CI time through cache hits without repeated builds |
| Gradual Adoption | Can start by adding a single turbo.json to an existing monorepo |
| Parallel Execution | Automatic parallelization of unrelated tasks with no additional configuration needed |
Disadvantages and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Vercel Dependency | Official remote cache requires a Vercel account; free plan has limitations | Use self-hosting (ducktors) or Cloudflare Workers |
| Cache Invalidation Complexity | Incorrect env or inputs configuration can cause incorrect cache hits |
Verify with --force flag to force rebuild after changes |
| Initial Setup Cost | Output paths must be accurately identified per task | Check artifact paths with ls -la after building first |
| Non-Deterministic Builds | Builds including timestamps or random values have low cache efficiency | Minimize dynamic value injection in build artifacts |
| Secret Management | Risk of sensitive variables in the env field being exposed in cache metadata |
Separate sensitive values using passThroughEnv |
One pitfall that actually caught me off guard: I once accidentally cached a deploy task. I clearly ran turbo deploy but nothing happened — a baffling situation. A cache hit had occurred, making it appear completed without an actual deployment. This happens when you forget "cache": false.
passThroughEnv: A field added in Turborepo 2.0. It passes environment variables to the build process only without including them in hash calculation. Useful for isolating security-related secrets from the cache hashing logic.
Most Common Mistakes in Practice
-
Leaving
outputsas an empty array or omitting it — This is the #1 cause of "why isn't the cache working?" Withoutoutputs, file artifact caching doesn't work at all. It's recommended to check the paths generated after building and specify them precisely. -
Forgetting
"cache": falseon thedeploytask — If a deploy task is cached, it appears to complete without an actual deployment. In situations where redeployment is needed, a cache hit can occur and nothing happens. -
Omitting
!.next/cache/**from theoutputsexclusion list — Next.js's internal cache and Turborepo's cache can conflict, making build results unpredictable. For Next.js projects, it's strongly recommended to always exclude with!.next/cache/**.
Wrapping Up
A single outputs setting in turbo.json and two lines to connect remote cache can completely transform the build experience for your entire team. The key is three things: "what files to look at when creating the hash (inputs), what artifacts to store (outputs), and what environment variables to include in the hash (env)" — get these three right and build speeds will visibly improve. That said, keep in mind that additional issues like inter-package type dependencies or non-deterministic builds may need to be addressed separately.
Three steps you can start right now:
- Specify
outputspaths inturbo.json— Check your build artifact paths withls -la distorls -la .next, then add them. You can immediately verify the change in cache hit rate using the--summarizeoption. - Connect Vercel remote cache with
turbo login && turbo link— Team-level cache sharing is available even on the free plan. The experience of receiving a teammate's build result directly on your local machine immediately makes the reason for adopting this self-evident. - Add
TURBO_TOKENandTURBO_TEAMenvironment variables to CI — For GitHub Actions, two lines is all it takes. From the first CI run onwards, builds with the same hash will be handled as cache hits.
References
- Turborepo Official Docs — Remote Caching
- Turborepo Official Docs — Caching
- Turborepo Official Docs — Configuring turbo.json
- Turborepo Official Docs — Using Environment Variables
- Vercel Official — Remote Caching
- Vercel Blog — Iterate faster with Turborepo and Vercel Remote Cache
- DEV.to — Turborepo 2.0: Remote Caching, Task Pipelines, and What Actually Speeds Up CI
- GitHub — ducktors/turborepo-remote-cache (Open-Source Self-Hosting)
- Leapcell — Optimizing CI/CD with Turborepo's Remote Caching
- johal.in — 2026 Monorepo Setup with Turborepo 2.0, pnpm 8.15, Next.js 15