Skip to content

Turborepo Reference: Task Caching, Remote Cache, Filtering & Monorepo Packages

Turborepo is a high-performance build system for JavaScript/TypeScript monorepos. Its core value: task caching — it hashes inputs (source files, env vars, dependencies) and skips tasks whose inputs haven’t changed. Run turbo build in CI after a minor change, and only the affected packages rebuild. Remote caching with Vercel or a self-hosted cache server extends this across machines and CI runs. The key file is turbo.json — it defines which tasks exist, their dependencies, and what to cache.

1. Workspace Setup & turbo.json

Initialize monorepo, workspace config, turbo.json tasks, and package.json scripts
# Create new monorepo:
npx create-turbo@latest
# Or add Turborepo to an existing monorepo:
npx turbo@latest init

# Monorepo structure:
# .
# ├── turbo.json          ← task definitions
# ├── package.json        ← workspace root
# ├── packages/
# │   ├── ui/             ← shared component library
# │   ├── eslint-config/  ← shared ESLint config
# │   └── tsconfig/       ← shared TypeScript config
# └── apps/
#     ├── web/            ← Next.js app
#     └── api/            ← Express/Hono API

# package.json (root):
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "build":   "turbo build",
    "dev":     "turbo dev",
    "lint":    "turbo lint",
    "test":    "turbo test",
    "type-check": "turbo type-check"
  },
  "devDependencies": {
    "turbo": "latest"
  }
}

# turbo.json — task pipeline:
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],  // ^ = run deps' build first (topological)
      "outputs": [".next/**", "dist/**", "build/**"],
      "cache": true
    },
    "dev": {
      "cache": false,           // never cache dev servers
      "persistent": true        // long-running process
    },
    "lint": {
      "dependsOn": [],          // can run in parallel, no deps
      "outputs": []
    },
    "test": {
      "dependsOn": ["build"],   // test after build
      "outputs": ["coverage/**"],
      "cache": true
    },
    "type-check": {
      "dependsOn": ["^build"],
      "outputs": []
    }
  }
}

2. Task Caching & Inputs

How caching works, customising inputs, env var inclusion, and cache hit behaviour
// Turborepo caches based on a hash of:
// - Source files in the package (respects .gitignore)
// - package.json + lockfile
// - turbo.json task config
// - Environment variables listed in "env"
// - Output of dependent tasks

// Cache a task with specific inputs:
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"],
      "inputs": ["src/**", "!src/**/*.test.ts"],  // exclude test files from build hash
      "env": ["NODE_ENV", "API_URL"],              // include env vars in hash
      "passThroughEnv": ["CI", "VERCEL"]           // available to task but not in hash
    },
    "test": {
      "outputs": ["coverage/**"],
      "inputs": ["src/**", "tests/**", "jest.config.*"],
      "env": ["CI"]
    }
  }
}

// Cache outputs are stored in:
// .turbo/cache/ — local file system cache
// Remote cache (Vercel, self-hosted) — shared across CI machines

// On cache HIT: Turborepo restores output files and replays logs. The task doesn't run.
// On cache MISS: Task runs, outputs are cached for next time.

// Force re-run (bypass cache):
turbo build --force

// See what would run without running:
turbo build --dry-run

// Profile task timing:
turbo build --profile=timing.json
npx @turbo/ui timing.json   // visualise in browser

3. Remote Caching

Vercel remote cache, self-hosted cache server, and CI setup
# Remote cache: share cache hits across team members and CI
# Default: Vercel (free for personal/hobby, paid for teams):
npx turbo login
npx turbo link   # link to Vercel team

# CI: set env vars, no interactive login needed:
# TURBO_TOKEN=your-vercel-token
# TURBO_TEAM=your-vercel-team-slug

# GitHub Actions with remote cache:
# - name: Build
#   run: turbo build
#   env:
#     TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
#     TURBO_TEAM: ${{ vars.TURBO_TEAM }}

# Self-hosted remote cache (no Vercel dependency):
# Option 1: ducktape (open source, drop-in replacement)
# Option 2: Turborepo remote cache server protocol (any S3-compatible store)

# docker-compose.yml for self-hosted ducktape cache:
# services:
#   turbo-cache:
#     image: fox1t/ducktape:latest
#     environment:
#       TURBO_TOKEN: "your-secret-token"
#     volumes: ["./cache:/app/cache"]
#     ports: ["3000:3000"]

# Use self-hosted cache:
turbo build --api="http://your-cache-server:3000" --token="your-secret-token" --team="team"
# Or in turbo.json:
# { "remoteCache": { "apiUrl": "http://your-cache-server:3000", "preflight": true } }

4. Filtering & Running Tasks

–filter to run tasks for specific packages, affected packages, and parallel execution
# Run tasks in all packages:
turbo build                    # build everything
turbo build --parallel         # force parallel (ignores dependsOn ordering)

# --filter: target specific packages:
turbo build --filter=web       # only the "web" app
turbo build --filter=@acme/*   # all packages in the @acme scope
turbo build --filter=!ui       # everything EXCEPT ui

# Affected packages (based on git diff from main):
turbo build --filter=[main]    # packages changed since main branch
turbo build --filter=[HEAD^]   # packages changed since last commit
turbo build --filter=[HEAD^]... # changed packages AND their dependents

# Run multiple tasks:
turbo lint test type-check     # run all three tasks

# Run task only in specific package (equivalent to package's own script):
turbo run build --filter=web

# Interactive mode — select packages to run:
turbo build --filter=...

# Watch mode (dev):
turbo dev   # all dev servers, restarts affected packages on change

# Package-level filtering — run a task in a package + all its dependents:
turbo build --filter=...ui     # ui + everything that depends on ui

5. Package Structure & Sharing Code

Internal packages, TypeScript configs, shared UI library, and common pitfalls
// packages/ui/package.json — internal UI library:
{
  "name": "@myapp/ui",
  "version": "0.0.0",
  "private": true,
  "exports": {
    ".": "./src/index.ts"  // just-in-time (JIT) compilation — no build step needed
  },
  "devDependencies": {
    "typescript": "latest",
    "@myapp/tsconfig": "*"  // internal tsconfig package
  }
}

// packages/tsconfig/package.json:
{
  "name": "@myapp/tsconfig",
  "version": "0.0.0",
  "private": true,
  "exports": {
    "./react": "./react.json",
    "./base": "./base.json"
  }
}
// packages/tsconfig/base.json:
// { "compilerOptions": { "strict": true, "moduleResolution": "bundler" } }

// apps/web/tsconfig.json — extends shared config:
{
  "extends": "@myapp/tsconfig/react",
  "compilerOptions": { "outDir": "dist" }
}

// Import internal package:
import { Button } from "@myapp/ui";   // resolved directly to src/index.ts

// Common pitfalls:
// 1. "exports" must match the actual file path — wrong path = silent failure
// 2. JIT packages (just src/) skip the build step but need all consumers to compile them
// 3. Side effects: mark UI packages as "sideEffects": false for better tree-shaking
// 4. Missing peer deps: if ui uses React, declare React as peerDependency not devDependency
// 5. turbo build --filter=... is slow first run — normal, building the cache

Track Node.js toolchain releases at ReleaseRun. Related: TypeScript Reference | Next.js App Router Reference | GitHub Actions Reference

🔍 Free tool: npm Package Health Checker — check turbo and monorepo tooling packages for known CVEs and active maintenance.

Founded

2023 in London, UK

Contact

hello@releaserun.com