Fix: Turborepo Not Working — Cache Never Hits, Pipeline Not Running, or Workspace Task Fails
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix Turborepo issues — turbo.json pipeline configuration, cache keys, remote caching setup, workspace filtering, and common monorepo task ordering mistakes.
The Problem
Turborepo never uses the cache — every build reruns from scratch:
turbo run build
# cache miss, executing ← every single timeOr a task fails because it runs before its dependency:
turbo run test
# Error: Cannot find module '@my-app/utils'
# (utils package hasn't been built yet)Or turbo run build --filter=web builds more (or fewer) packages than expected:
turbo run build --filter=web
# Also builds unrelated packages — or misses necessary dependenciesOr remote caching isn’t working in CI despite being configured:
# CI logs show: "Remote caching disabled"Why This Happens
Turborepo’s behavior depends heavily on correct turbo.json configuration. The pipeline is the source of truth for what runs, when it runs, and what gets cached. Get it wrong and every command either skips the cache, runs tasks out of order, or misses entire packages.
- Cache keys include all inputs — if the cache always misses, either the inputs are constantly changing (e.g., a timestamp in a generated file), or the
inputsconfiguration isn’t narrow enough to exclude irrelevant files. - Task dependencies must be declared —
"dependsOn": ["^build"]means “run thebuildtask of all workspace dependencies first.” Without this, tasks run in parallel without waiting for dependencies. --filteruses package names, not directory paths —--filter=weblooks for a package namedwebinpackage.json, not theweb/directory. If your package is named@my-app/web, use--filter=@my-app/web.- Remote caching requires a valid token — without
TURBO_TOKENandTURBO_TEAMenvironment variables (or Vercel authentication), remote cache falls back to local-only.
A second source of failures is Turborepo’s interaction with the package manager. Turborepo does not install or link workspaces itself — it shells out to npm, pnpm, yarn, or bun and reads the resulting lockfile to build the task graph. When the lockfile is out of sync, when workspace:* protocol is used but the manager isn’t pnpm or bun, or when the dependent package never declares the dependency in its own package.json, the graph is missing edges. Turbo then runs tasks in the wrong order with no warning.
Migrations from turbo@1 to turbo@2 also trip teams: pipeline was renamed to tasks, dependsOn semantics around env vars changed, and the cache hash now includes the resolved environment variable values rather than just their names. If you upgraded recently and every cache started missing, this is usually why.
Fix 1: Configure turbo.json Correctly
The turbo.json pipeline defines task ordering, caching, and outputs:
// turbo.json — at the monorepo root
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [".env"], // Cache invalidated when .env changes
"tasks": {
// Build task — depends on dependencies' builds first
"build": {
"dependsOn": ["^build"], // ^ means "dependency packages' build"
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"inputs": ["src/**", "package.json", "tsconfig.json"]
},
// Test — depends on this package's own build
"test": {
"dependsOn": ["build"], // No ^ = same package's build
"outputs": ["coverage/**"],
"inputs": ["src/**", "tests/**", "jest.config.*"]
},
// Lint — no task dependencies, fully parallel
"lint": {
"outputs": [], // No outputs to cache
"inputs": ["src/**", ".eslintrc.*", ".eslintignore"]
},
// Dev server — never cache, always run
"dev": {
"cache": false,
"persistent": true // Long-running task — turbo won't wait for it to finish
},
// Type check
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
}
}
}Common dependsOn patterns:
{
"tasks": {
"build": {
"dependsOn": ["^build"]
// ^ = all dependency packages must finish their "build" first
},
"test": {
"dependsOn": ["build"]
// No ^ = this package's own "build" must finish first
},
"deploy": {
"dependsOn": ["build", "test", "^deploy"]
// Own build+test done, AND dependency packages' deploy done
}
}
}Fix 2: Fix Cache Always Missing
Diagnose why the cache never hits:
# Run with verbose output to see cache key details
turbo run build --verbosity=2
# Dry run — shows what would run without executing
turbo run build --dry=json | jq '.tasks[] | {task: .task, package: .package, cache: .cache}'
# Force a cache miss to regenerate
turbo run build --force
# Check what's included in the cache hash
TURBO_LOG_ORDER=stream turbo run build 2>&1 | grep "hash"Common cache miss causes:
// PROBLEM 1: outputs not specified — nothing to cache
{
"tasks": {
"build": {
"dependsOn": ["^build"]
// Missing "outputs" — cache always misses because nothing is saved
}
}
}
// FIX: specify what the build produces
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"] // These files are cached and restored
}
}
}
// PROBLEM 2: inputs too broad — every file change invalidates cache
{
"tasks": {
"test": {
"outputs": ["coverage/**"]
// No "inputs" = ALL files are inputs — README.md change busts the cache
}
}
}
// FIX: narrow the inputs
{
"tasks": {
"test": {
"outputs": ["coverage/**"],
"inputs": ["src/**", "tests/**", "*.config.*", "package.json"]
}
}
}Exclude generated files from cache inputs:
{
"tasks": {
"build": {
"inputs": [
"src/**",
"public/**",
"!src/**/__generated__/**", // Exclude generated files
"!src/**/*.stories.tsx", // Exclude storybook files
"package.json",
"tsconfig.json"
],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
}
}
}Fix 3: Filter Tasks to Specific Packages
Use --filter to run tasks for specific packages and their dependencies:
# Run by package name (from package.json "name" field)
turbo run build --filter=@my-app/web
# Multiple packages
turbo run build --filter=@my-app/web --filter=@my-app/api
# By directory (use ./ prefix)
turbo run build --filter=./apps/web
# Include all dependencies of 'web' (packages web depends on)
turbo run build --filter=@my-app/web...
# Include all dependents of 'utils' (packages that depend on utils)
turbo run build --filter=...@my-app/utils
# Run only on changed packages (compared to main branch)
turbo run test --filter=[main]
# Run only on packages changed since last commit
turbo run test --filter=[HEAD^1]Package naming — always check package.json:
// apps/web/package.json
{
"name": "@my-app/web", // This is the filter name, not the directory
"scripts": {
"build": "next build"
}
}
// WRONG — directory name without @scope
turbo run build --filter=web
// CORRECT — full package name from package.json
turbo run build --filter=@my-app/webFix 4: Set Up Remote Caching
Remote caching shares the build cache across CI runs and team members:
# Option 1: Vercel Remote Cache (free, official)
# Login to Vercel
npx turbo login
# Link the repo to a Vercel team
npx turbo link
# Now CI builds automatically use the remote cache
# Set these in CI environment:
# TURBO_TOKEN — from Vercel dashboard → Settings → Tokens
# TURBO_TEAM — your Vercel team slug# GitHub Actions — pass Turborepo remote cache credentials
jobs:
build:
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Required for --filter=[HEAD^1]
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx turbo run build test lint --filter=[HEAD^1]Self-hosted remote cache (Ducktape/Turborepo Remote Cache):
# Run a self-hosted cache server
docker run -p 3000:3000 \
-e STORAGE_PROVIDER=local \
-e STORAGE_PATH=/data \
-e TURBO_TOKEN=my-secret-token \
ducktape/turborepo-remote-cache
# Point turbo at the custom server
# turbo.json
{
"remoteCache": {
"apiUrl": "http://your-cache-server:3000"
}
}
# Or via environment variable
TURBO_API=http://your-cache-server:3000 turbo run buildFix 5: Workspace Setup
Turborepo works on top of your package manager’s workspaces:
// Root package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"test": "turbo run test",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "^2.0.0"
}
}my-monorepo/
├── turbo.json
├── package.json
├── apps/
│ ├── web/
│ │ └── package.json {"name": "@my-app/web"}
│ └── api/
│ └── package.json {"name": "@my-app/api"}
└── packages/
├── ui/
│ └── package.json {"name": "@my-app/ui"}
└── utils/
└── package.json {"name": "@my-app/utils"}Cross-package dependencies:
// apps/web/package.json
{
"name": "@my-app/web",
"dependencies": {
"@my-app/ui": "*", // Reference by workspace path
"@my-app/utils": "*"
}
}
// packages/ui/package.json
{
"name": "@my-app/ui",
"main": "./dist/index.js",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts"
}
}Fix 6: Debug Common Errors
“Package not found” in dependent package:
# Ensure the dependency is built before the dependent
# turbo.json must have:
# "build": { "dependsOn": ["^build"] }
# Verify workspace linking
npm ls @my-app/utils # or pnpm ls / bun pm ls
# Check that the built output exists
ls packages/utils/dist/Task hangs indefinitely:
# Add timeout to prevent CI hangs
turbo run build --concurrency=4 # Limit parallel tasks
# Check for circular dependencies
turbo run build --graph # Generates a dependency graph (requires graphviz)
turbo run build --graph=graph.pngpnpm workspace issues:
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'// turbo.json — specify package manager for pnpm
{
"$schema": "https://turbo.build/schema.json",
"tasks": { }
}
// No special config needed — turbo auto-detects pnpm from lockfile# pnpm + turbo — install dependencies
pnpm install
# Run turbo via pnpm
pnpm turbo run build
# or add to root package.json scripts and run: pnpm buildTurborepo vs Nx vs Lerna vs Rush vs pnpm Workspaces vs Moon
Turborepo is one of several monorepo orchestrators in active use. Picking the wrong tool, or using two that overlap, is a common reason builds feel slow or behave inconsistently.
Turborepo wraps your package manager. It does not handle dependency installation, workspace linking, or code generation. Its job is task scheduling and caching. The configuration surface is small — a single turbo.json with tasks, inputs, outputs, dependsOn. Remote caching is plug-and-play via Vercel. The Go binary starts in under 50ms, which makes it cheap to use even for tiny tasks. It does not have generators, plugins, or affected-graph commands beyond --filter=[base].
Nx is a full monorepo platform. It owns generators (nx g @nx/react:app web), affected-graph commands (nx affected -t test), dependency-graph visualization, task pipelines via targetDefaults, and computation caching with Nx Cloud. Nx is heavier — the daemon, the project graph, the plugin ecosystem — but pays for itself once you have 20+ packages, multiple languages, or a need for code generation. Nx caches finer-grained inputs than Turborepo because the project graph is computed from source imports, not just package.json.
Lerna is the original. Since v6 (relaunched by Nx), it delegates task running to Nx under the hood. If you are starting fresh in 2026, use Nx directly. Lerna is mostly used for its lerna publish command, which automates versioning and changelog generation across packages — Nx and Turborepo have no equivalent and usually pair with Changesets instead.
Rush (Microsoft) targets very large monorepos (hundreds of packages). It enforces stricter conventions: a phantom-dependency-free install via pnpm symlink mode, mandatory rush.json configuration, build phases, and incremental publishing. Rush is overkill for under ~50 packages but the only tool that scales to Microsoft-internal-sized repos. Build caching uses Azure Storage, AWS S3, or local.
pnpm workspaces alone (no orchestrator) gives you pnpm -r run build and pnpm --filter @scope/web build. No caching, no affected detection, no task pipelines beyond pnpm’s topological ordering. For a 2-5 package repo, this is often enough — adding Turborepo on top buys you incremental cache hits but adds a config file and a binary. The break-even is around the point where pnpm -r build takes longer than 10-15 seconds locally.
Moon (moonrepo) is the newest entrant. Like Turborepo, it is Rust-based and fast. Unlike Turborepo, it manages tool versions (node, deno, rust toolchains), supports non-JS languages first-class, and includes a project graph with affected detection. The config is more verbose than turbo.json and the community is smaller, but Moon handles polyglot monorepos (a Rust crate next to a Next.js app) more cleanly.
Rule of thumb: Turborepo for JS/TS monorepos that want fast task caching with minimal setup. Nx when you want generators, plugins, and a real project graph. Rush for very large enterprise monorepos. Moon for polyglot. pnpm workspaces alone for tiny repos.
Still Not Working?
Cache hits locally but misses in CI — the most common cause is non-deterministic inputs. Check for files that contain timestamps, random values, or absolute paths that differ between machines. Use turbo run build --dry=json in both environments and compare the hashOfExternalDependencies and inputs fields.
--filter=[HEAD^1] always runs all packages — this requires fetch-depth: 2 (or greater) in actions/checkout. With fetch-depth: 1 (shallow clone), there’s no parent commit to compare against, so Turbo conservatively runs everything. Set fetch-depth: 0 to get full history if you need accurate change detection across more than one commit.
Outputs not restored from cache — if turbo run build reports a cache hit but the dist/ folder is empty, the outputs pattern in turbo.json doesn’t match what the build actually produces. Run the build once, then ls dist/ to see the actual structure, and update the outputs glob accordingly.
Env vars in cache hash cause unexpected misses — Turborepo 2.x hashes the resolved values of any env vars declared in env or globalEnv. If your CI sets CI_BUILD_ID=12345 on every run and you list it in globalEnv, every build is a fresh hash. List only env vars that genuinely affect the output (NODE_ENV, NEXT_PUBLIC_*) and use passThroughEnv for runtime-only values that should not enter the hash.
workspace:* protocol not recognized — pnpm and bun support "dependency": "workspace:*". npm and yarn classic do not. If you copied a package.json from a pnpm repo into an npm repo, npm install rewrites or errors. Switch to pnpm or replace workspace:* with * plus a workspaces entry.
turbo daemon causes stale state on Windows — the Turbo daemon caches the file system glob results in memory. On Windows, file watching is sometimes flaky and the daemon serves stale data after a git checkout of a different branch. Run turbo daemon stop (or turbo --no-daemon run build for a single run) to bypass it.
For related monorepo issues, see Fix: npm ERR Peer Dep Conflict, Fix: pnpm Workspace Not Working, Fix: Nx Not Working, and Fix: TypeScript Path Alias Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: esbuild Not Working — Plugin Errors, CSS Not Processed, or Output Missing After Build
How to fix esbuild issues — entry points, plugin API, JSX configuration, CSS modules, watch mode, metafile analysis, external packages, and common migration problems from webpack.
Fix: Git Hooks Not Running — Husky Not Working, pre-commit Skipped, or lint-staged Failing
How to fix Git hooks not executing — Husky v9 setup, hook file permissions, lint-staged configuration, pre-commit Python tool, lefthook, and bypassing hooks in CI.
Fix: Docker Multi-Platform Build Not Working — buildx Fails, Wrong Architecture, or QEMU Error
How to fix Docker multi-platform build issues — buildx setup, QEMU registration, --platform flag usage, architecture-specific dependencies, and pushing multi-arch manifests to a registry.
Fix: GitHub Actions Artifacts Not Working — Upload Fails, Download Empty, or Artifact Not Found
How to fix GitHub Actions artifact issues — upload-artifact path patterns, download-artifact across jobs, retention days, artifact name conflicts, and the v3 to v4 migration.