Skip to content

Fix: Turborepo Not Working — Cache Never Hits, Pipeline Not Running, or Workspace Task Fails

FixDevs ·

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 time

Or 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 dependencies

Or 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:

  • 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 inputs configuration isn’t narrow enough to exclude irrelevant files.
  • Task dependencies must be declared"dependsOn": ["^build"] means “run the build task of all workspace dependencies first.” Without this, tasks run in parallel without waiting for dependencies.
  • --filter uses package names, not directory paths--filter=web looks for a package named web in package.json, not the web/ directory. If your package is named @my-app/web, use --filter=@my-app/web.
  • Remote caching requires a valid token — without TURBO_TOKEN and TURBO_TEAM environment variables (or Vercel authentication), remote cache falls back to local-only.

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/web

Fix 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 build

Fix 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.png

pnpm 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 build

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.

For related monorepo issues, see Fix: npm ERR Peer Dep Conflict and Fix: TypeScript Path Alias Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles