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!
}
}
}
The fix:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^build"],
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
}
}
}
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
GitHub Actions setup:
# .github/workflows/ci.yml
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
The gotcha that cost me half a day:
TURBO_TOKENneeds 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)
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!)
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)
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
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"
]
}
}
}
Critical: Listing env vars in
turbo.json'senvarray 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"
}
# Ensure same version in CI
corepack enable
corepack prepare pnpm@9.15.4 --activate
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"]
}
}
}
"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
outputsfor every task. Tasks with no output (like lint) get an empty array[]. -
Declare
envfor every env var that affects build output. Otherwise cache can serve stale builds. -
Pin your package manager version with
packageManagerfield + 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)