DEV Community

jake kim
jake kim

Posted on • Originally published at dev-jake.blogspot.com

Turborepo Monorepo Troubleshooting 2026: Why Your Cache Isn't Working and CI Keeps Failing

I lost an entire week setting up Turborepo in a monorepo after hearing it would "boost productivity." From "why is turbo run build always a MISS?" to "CI fails but local works fine" — here's every pitfall I hit, so you don't have to.

This was a Next.js + pnpm workspace monorepo with just 3 packages (web, admin, shared-ui). Relatively simple, and I still burned this much time. If you're running something more complex, this should save you a few days.


The Damage Report

Issue Time Lost Pain Level
Missing outputs config 3 hours ⭐⭐⭐⭐
Remote Cache auth Half a day ⭐⭐⭐⭐⭐
Circular dependency 2 hours ⭐⭐⭐
turbo watch TUI chaos 1 hour ⭐⭐
CI env variable mismatch Half a day ⭐⭐⭐⭐⭐

1. Missing outputs — The #1 Reason Cache Doesn't Work

I set up Turborepo, ran turbo run build, changed nothing, ran it again — full rebuild. Every single time. The log just said "MISS" over and over. Cache was useless.

The cause: I didn't set outputs in turbo.json. Turborepo caches based on the output folders you declare. If you don't tell it what to cache, it doesn't cache anything.

The broken config:

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"]
      // No outputs!
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The fix:

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "lint": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Key detail for Next.js: Include .next/** in outputs but exclude !.next/cache/**. Otherwise you're caching the cache, and the artifact size explodes.


2. Remote Cache Auth — Escaping the MISS Loop

Cache worked locally. CI (GitHub Actions)? MISS every time. Obviously — local cache lives on your machine. CI spins up fresh environments with nothing cached.

The fix is Vercel Remote Cache. Free if your project is linked to Vercel.

Local setup:

# 1. Link Vercel account
npx turbo login

# 2. Link project
npx turbo link
Enter fullscreen mode Exit fullscreen mode

GitHub Actions setup:

# .github/workflows/ci.yml
env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}
Enter fullscreen mode Exit fullscreen mode

The gotcha that cost me half a day: TURBO_TOKEN needs to be a Turborepo-specific token, NOT a Vercel Personal Access Token. I used my PAT and kept getting 403s without understanding why. Generate the token from Vercel Dashboard → Settings → Tokens, and make sure it's scoped for Turborepo.

TURBO_TEAM is your Vercel team slug (or username for personal accounts). Skip it and cache silently fails — no error, just endless MISSes.


3. Circular Dependencies — Why Build Order Breaks

My package structure looked like this:

packages/
  shared-ui/     → @repo/shared-ui
  web/           → @repo/web (depends on shared-ui)
  admin/         → @repo/admin (depends on shared-ui)
Enter fullscreen mode Exit fullscreen mode

Clean so far. The problem started when I added @repo/web as a devDependency in shared-ui to reference some types:

web → shared-ui → web (circular!)
Enter fullscreen mode Exit fullscreen mode

Turborepo can't resolve build order with circular deps. The error message isn't particularly helpful either, so it took a while to figure out.

Fix: Extract shared types into a dedicated @repo/types package. This is monorepo design 101, but I skipped it because "one more package felt overkill." Learned that lesson fast.

packages/
  types/         → @repo/types (shared types)
  shared-ui/     → @repo/shared-ui (depends on types)
  web/           → @repo/web (depends on shared-ui, types)
  admin/         → @repo/admin (depends on shared-ui, types)
Enter fullscreen mode Exit fullscreen mode

4. turbo watch Destroying Terminal Output

Running turbo watch dev triggers cascading restarts whenever you edit a dependency. In theory, correct behavior. In practice, tasks start → abort → restart repeatedly, and your terminal becomes unreadable.

Edit shared-ui, and both web and admin restart simultaneously. Error messages interleave. You can't tell real errors from transient restart noise.

My workaround: Separate terminals per app. Primitive, but stable.

# Terminal 1: shared-ui watch
pnpm --filter @repo/shared-ui dev

# Terminal 2: web
pnpm --filter @repo/web dev

# Terminal 3: admin
pnpm --filter @repo/admin dev
Enter fullscreen mode Exit fullscreen mode

Turborepo 2.x improved watch mode significantly, but deep dependency graphs still hit rough edges.


5. CI Builds Fail, Local Works Fine

Local turbo run build — passes. Same command in GitHub Actions — fails. This is the most soul-crushing scenario.

Cause 1: Missing environment variables

Next.js inlines NEXT_PUBLIC_* env vars at build time. If CI doesn't have them, the build might succeed but the app breaks at runtime. Some variables crash the build entirely.

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**"],
      "env": [
        "NEXT_PUBLIC_API_URL",
        "NEXT_PUBLIC_SENTRY_DSN",
        "DATABASE_URL"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Critical: Listing env vars in turbo.json's env array means the cache invalidates when those values change. Without this, you get stale builds — I once shipped a staging build with production API URLs baked in because the cache didn't know the env changed.

Cause 2: pnpm version mismatch

Local was pnpm 9.x, CI had pnpm 8.x. The lockfile format differs, causing intermittent install failures.

// package.json
{
  "packageManager": "pnpm@9.15.4"
}
Enter fullscreen mode Exit fullscreen mode
# Ensure same version in CI
corepack enable
corepack prepare pnpm@9.15.4 --activate
Enter fullscreen mode Exit fullscreen mode

6. Composable Config — Scaling turbo.json

As packages grow, a single turbo.json gets unwieldy. Since Turborepo 2.7, Composable Configuration lets you distribute configs per package:

// apps/web/turbo.json
{
  "extends": ["//"],
  "tasks": {
    "build": {
      "outputs": [".next/**", "!.next/cache/**"],
      "env": ["NEXT_PUBLIC_API_URL"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

"extends": ["//"]" inherits from the root turbo.json. Each app can declare its own env vars and outputs cleanly.


Survival Checklist

Before you go, run through this:

  • Declare outputs for every task. Tasks with no output (like lint) get an empty array [].
  • Declare env for every env var that affects build output. Otherwise cache can serve stale builds.
  • Pin your package manager version with packageManager field + corepack.
  • No circular deps. Shared types get their own package. No bidirectional dependencies, ever.
  • Verify your Remote Cache token. It's a Turborepo token, not a Vercel PAT.

Verdict: Is Turborepo Worth It?

Despite all the pain — yes. Once configured properly, CI build times dropped roughly in half. Local rebuilds became near-instant for unchanged packages. The DX improvement is real.

But "just install it" is a lie. Get outputs, env, and Remote Cache wrong, and the cache is decoration. Mess up the dependency graph, and build order falls apart. Dodge the pitfalls in this post, and you'll have a much smoother setup.


Originally published on dev.Jake. I write about frontend tooling, build systems, and things that took too long to debug.

Top comments (0)