jsmanifest logojsmanifest

Turbo: Supercharge Your Monorepo Build Performance

Turbo: Supercharge Your Monorepo Build Performance

Learn how Turborepo's intelligent caching and parallelization can transform your monorepo builds from painfully slow to lightning fast with practical configuration examples.

While I was looking over some monorepo builds the other day, I watched a CI pipeline take 18 minutes to complete what should have been a 3-minute job. The developer had added one line of code to a utility package, and the entire workspace was rebuilding everything from scratch. I cannot stress this enough—if you're running a monorepo without Turborepo, you're leaving massive performance gains on the table.

Why Your Monorepo Builds Are Painfully Slow (And How Turborepo Fixes It)

I was once guilty of thinking that throwing more CI workers at the problem would solve slow builds. Little did I know that the real issue wasn't computational power—it was unnecessary work. Most monorepo setups rebuild everything on every change, even when 90% of your packages haven't touched a single file.

Here's what typically happens without Turborepo: You change a button component in your design system. Your build system sees this change and decides to rebuild your API package, your documentation site, your mobile app config, and everything else that depends on nothing related to that button. It's wasteful and frustrating.

Turborepo solves this through two key innovations: intelligent caching and task orchestration. It tracks what actually changed and only rebuilds what's affected. When I finally decided to implement it in a project with 23 packages, our average build time dropped from 14 minutes to 2 minutes. The ROI was immediate.

Understanding Turborepo's Intelligent Caching System

The magic behind Turborepo's speed is its content-aware hashing system. It creates a fingerprint of your task inputs—source files, dependencies, environment variables, and configuration—then checks if it's already computed that exact work before. If it finds a match, it restores the cached output instead of rebuilding.

In other words, if you run turbo build and nothing changed in a package, Turborepo skips that build entirely and gives you the cached result. This happens locally on your machine and can extend to your entire team through remote caching.

Here's the fascinating part: Turborepo doesn't just cache the final build artifacts. It caches the entire task output, including logs. This means when you restore from cache, you even see the original build logs, making debugging significantly easier.

Turborepo caching workflow

Configuring Turborepo Pipeline for Maximum Parallelization

Let's look at a real configuration that transformed one of my projects. The key is defining your pipeline in turbo.json to tell Turborepo about task dependencies and what outputs to cache:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "build/**"],
      "env": ["NODE_ENV"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"],
      "cache": true
    },
    "lint": {
      "outputs": [],
      "cache": true
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

The ^build notation in dependsOn is crucial. That caret symbol means "build all dependencies first." When you run turbo build, Turborepo looks at your package dependency graph and builds packages in the optimal order, running independent packages in parallel.

I came across a common mistake here that cost me hours of debugging: forgetting to specify all outputs. If your build produces files that aren't listed in outputs, they won't be cached. You'll get cache hits but missing artifacts, leading to confusing errors downstream.

Remote Caching: Share Build Artifacts Across Your Team

Local caching is wonderful, but remote caching is where the real productivity gains happen. Imagine this scenario: Your teammate just merged a PR that built successfully on CI. When you pull their changes and run the build, instead of rebuilding everything, Turborepo fetches the cached artifacts from your shared remote cache. Your build completes in seconds.

Setting up remote caching with Vercel's service is straightforward:

// Authenticate with Vercel
// Run: npx turbo login
 
// Link your repository
// Run: npx turbo link
 
// Your turbo.json automatically uses remote cache
// No additional configuration needed!
 
// For self-hosted solutions, you can use S3, Azure Blob, or any
// cloud storage that implements the Remote Cache API
 
// Example environment setup for custom remote cache:
// TURBO_API=https://your-cache-api.com
// TURBO_TOKEN=your-secret-token
// TURBO_TEAM=your-team-id

When I finally decided to enable remote caching for a team of 8 developers, we reduced our total daily build time from approximately 6 hours (across all developers) to under 45 minutes. The first person to build after a change does the work, and everyone else gets instant results.

Luckily we can also use remote caching in CI/CD environments. Your CI builds become much faster on subsequent runs because Turborepo can restore from cache instead of rebuilding. I've seen CI pipelines go from 15 minutes to 3 minutes just by enabling remote caching.

Remote caching workflow diagram

Advanced Performance Tuning: Task Dependencies and Hashing

Understanding what Turborepo includes in its hashing is critical for optimization. By default, it hashes all files in your package directory, which can include unnecessary files that trigger cache misses.

Here's a more sophisticated configuration that fine-tunes what gets hashed:

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": [
    ".eslintrc.js",
    "tsconfig.base.json",
    "jest.config.base.js"
  ],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"],
      "inputs": ["src/**/*.ts", "src/**/*.tsx", "package.json"],
      "env": ["NODE_ENV", "API_URL"],
      "outputMode": "hash-only"
    },
    "test:unit": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.test.ts", "src/**/*.spec.ts"],
      "outputs": ["coverage/**"],
      "cache": true
    }
  }
}

The inputs field tells Turborepo to only hash specific files. In this example, the build task only considers TypeScript files in src/ and package.json. Changes to README files, test files, or documentation won't trigger a rebuild.

The globalDependencies array is equally important. These files affect all tasks across all packages. When you update your root ESLint config, Turborepo knows to invalidate all caches because linting rules changed.

I cannot stress this enough: incorrect inputs configuration leads to either unnecessary rebuilds (too broad) or stale caches (too narrow). Start broad and narrow down based on your actual build requirements.

Measuring Impact: Before vs After Turborepo Optimization

Before Turborepo, a typical workflow looked like this:

  • Initial build: 12 minutes
  • Rebuild after changing one file: 11 minutes
  • CI build on PR: 14 minutes
  • Cache hit rate: 0%

After implementing Turborepo with remote caching:

  • Initial build: 8 minutes (improved by cleanup)
  • Rebuild after changing one file: 45 seconds
  • CI build on PR with cache: 2 minutes
  • Cache hit rate: 85-95%

The key metric I track is "time to productive feedback." How long does a developer wait to see if their change broke something? With Turborepo, that time dropped from 12 minutes to under 1 minute for most changes.

Common Performance Bottlenecks and How to Fix Them

Through working with Turborepo across multiple projects, I've identified recurring bottlenecks:

Problem: Low cache hit rates despite no changes This usually means your hashing is including volatile files. Check for timestamp-based files, logs, or dynamic imports that change on every build. Use the inputs field to exclude them.

Problem: Builds still slow even with caching You might have sequential task dependencies that prevent parallelization. Look at your dependsOn chains. Do you really need lint to run before build? Often these can run in parallel.

Problem: Remote cache seems slower than local builds Network latency to your remote cache matters. If you're using a provider in a different region, consider moving your cache closer to your team or CI runners. I once discovered our cache was in US-West while our team was in Europe—switching regions cut cache fetch time by 70%.

Production-Ready Turborepo: CI/CD Integration Best Practices

For CI/CD, I recommend this GitHub Actions setup that maximizes cache effectiveness:

name: CI
on: [push, pull_request]
 
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # Full history for better diffing
      
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'
      
      - run: pnpm install --frozen-lockfile
      
      - name: Build with Turbo
        run: pnpm turbo build test lint
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
      
      - name: Show cache stats
        run: pnpm turbo run build --summarize

The --summarize flag gives you detailed cache analytics. I review these weekly to identify packages with poor cache hit rates and optimize their configuration.

In other words, treat your build performance as a product metric. Track it, measure it, and continuously optimize it. The developer experience improvements are worth the investment.

And that concludes the end of this post! I hope you found this valuable and look out for more in the future!