Turborepo’s build --filter command is the magic bullet for monorepo build times, but it’s not just about running build on fewer things.

Let’s see it in action. Imagine this packages/ directory structure:

packages/
├── ui/
│   ├── src/
│   │   └── Button.tsx
│   └── package.json (build script: "tsc")
├── utils/
│   ├── src/
│   │   └── format.ts
│   └── package.json (build script: "tsc")
├── web/
│   ├── src/
│   │   └── pages/
│   │       └── index.tsx
│   └── package.json (build script: "next build")
└── api/
    ├── src/
    │   └── index.ts
    └── package.json (build script: "esbuild src/index.ts --bundle --outfile=dist/index.js")

And a turbo.json like this:

{
  "$schema": "https://turborepo.org/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    }
  }
}

Normally, running turbo run build would trigger tsc for ui and utils, esbuild for api, and next build for web, respecting the dependency graph.

Now, let’s say you only changed packages/ui/src/Button.tsx. If you ran turbo run build --filter=ui, you’d expect only ui to build. But Turborepo is smarter. It sees that web depends on ui (via dependsOn: ["^build"] and packages/web/package.json listing ui as a dependency). Because ui has changed, Turborepo will rebuild ui and then rebuild web to incorporate the updated ui package.

turbo run build --filter=ui

The output might look like this:

• Packages in scope: 4
• Running build in 4 packages
• Affected 2 targets
  • ui:build: 1.234s
  • web:build: 5.678s

This is the core concept: Turborepo’s --filter isn’t just a simple include/exclude. It’s a dependency-aware filter. When you filter for a package, Turborepo analyzes the entire dependency graph. If the filtered package has changed, it will rebuild that package and any downstream packages that depend on it (directly or indirectly).

The turbo.json outputs field is crucial here. Turborepo uses these declared outputs to determine if a package’s build is stale. If packages/ui/dist/index.js doesn’t exist, or if its timestamp is older than the source files it depends on, Turborepo knows it needs to be rebuilt, even if no direct changes were made to ui’s source files. This caching mechanism is what makes incremental builds so fast.

So, how do you control exactly what gets built? The --filter flag accepts several syntaxes:

  • Package name: turbo run build --filter=ui (builds ui and its dependents)
  • Glob patterns: turbo run build --filter=./packages/ui/... (builds packages matching the glob and their dependents)
  • ^ prefix for dependencies: turbo run build --filter=^web (builds web and all its direct dependencies, but not web itself or its dependents)
  • ! for negation: turbo run build --filter=./packages/.../!./packages/ui (builds all packages in packages except ui and their dependents)

The most powerful combination is often turbo run build --filter=./packages/web/.... This tells Turborepo: "I want to build web, and anything that web depends on (recursively), but only if those things have actually changed or if their outputs are missing/stale." This is often the sweet spot for development, where you’re focused on a specific part of the application.

The real magic happens when you combine --filter with Turborepo’s caching. If you run turbo run build --filter=ui and ui hasn’t changed, Turborepo will skip the build entirely and return a cache hit.

• Packages in scope: 4
• Running build in 4 packages
• Affected 0 targets
  • ui:build: 0ms (cached)
  • web:build: 0ms (cached)

The dependsOn in turbo.json is what establishes the upstream build requirements. When you specify ^build, it means that before a package’s build script runs, all of its dependencies’ build scripts must have run successfully. Turborepo infers the exact dependency graph from your package.json files.

The one thing most people don’t realize is that --filter is not just about which packages to run the command on, but also which packages Turborepo considers "affected" in the context of a change. If you change a file within packages/ui, Turborepo marks ui as changed. Then, when you run turbo run build --filter=web, Turborepo checks if web depends on ui. It does. So, even though you filtered for web, Turborepo knows that ui’s change affects web’s build outcome, and will trigger ui’s build (if needed) before web’s.

This dependency-aware filtering is why you can confidently make changes in one part of your monorepo and only rebuild what’s necessary, without manually tracking upstream impacts.

The next logical step is understanding how to integrate this into your CI/CD pipeline to only deploy what has changed.

Want structured learning?

Take the full Vercel course →