| | |

pnpm 09 📦 Filtering Workspaces and Running Parallel Tasks

⏱️ Estimated read: 8 min | |

A monorepo with ten workspaces requires orchestration. Running pnpm test in each workspace manually is tedious. Running it in every workspace simultaneously overloads the machine. The --filter flag selects which workspaces to target, and the --parallel and --workspace-concurrency flags control how many run at once. The filter syntax is more than a simple name matcher—it supports dependency traversal (...), dependent traversal (...^), directory patterns, and git-based change detection. Mastering this syntax is the difference between a CI pipeline that takes three minutes and one that takes thirty.

Key point: --filter <name>... includes the named workspace and its dependencies. --filter ...<name> includes the named workspace and its dependents. The trailing or leading ellipsis determines the direction of the traversal.


Why filtering and parallel execution matter

The unnecessary work problem. In a monorepo with fifty packages, a change to one package only requires testing that package and the packages that depend on it. Running the full test suite for every change wastes CI minutes and developer time. The --filter flag with git-based selectors ([origin/main]) limits execution to workspaces that changed since a specific commit.

The dependency order problem. If @myorg/app depends on @myorg/utils, building @myorg/utils before @myorg/app is mandatory. The ... suffix in the filter ensures that dependencies are included in the execution set. By default, pnpm runs the selected workspaces in topological order—dependencies first, dependents last. This prevents build failures caused by missing artifacts.

The CPU saturation problem. Running ten test suites simultaneously on a four-core machine degrades performance for every suite. The --workspace-concurrency flag limits the number of workspaces that execute at the same time. The default is 4 for run, but for pnpm -r exec it defaults to the number of CPU cores. Setting it to a lower value (e.g., 2) keeps the machine responsive and avoids test timeouts caused by CPU starvation.

The parallel vs topological problem. The --parallel flag ignores topological order and runs every selected workspace simultaneously. This is faster but only safe when the tasks are independent—linting, for example, does not require a build artifact. The default behavior respects dependencies, which is correct for build tasks but slower.

The trade-off. Filtering is precise but requires understanding the syntax. Parallel execution is fast but can fail when dependencies are not respected. The correct choice depends on the task.


a. Filtering by name and directory

The --filter (-F) flag selects workspaces. The simplest form is a name match. The match is against the name field in each workspace’s package.json, not the directory name.

# Run test in a specific workspace
pnpm --filter @myorg/utils run test

# Run test in all workspaces under apps/
pnpm --filter "./apps/*" run test

# Exclude a workspace
pnpm --filter "!@myorg/docs" run test

The directory pattern ./apps/* matches the workspace’s physical location. The name pattern @myorg/utils matches the name field. Both forms can be combined with exclusions. The ! prefix negates the filter.

Filters can also match by the presence of a dependency. --filter "...^@myorg/utils" selects every workspace that depends on @myorg/utils, including transitive dependents. This is useful for running the full downstream test suite after changing a shared library.


b. Traversal filters: dependencies and dependents

The ellipsis (...) is the traversal operator. It includes dependencies or dependents.

# Named workspace and its dependencies
pnpm --filter @myorg/app... run build

# Named workspace and its dependents
pnpm --filter ...@myorg/utils run test

The direction matters. @myorg/app... means “app and everything it depends on.” ...@myorg/utils means “everything that depends on utils, plus utils itself.” The first is for building. The second is for testing downstream consumers.

The syntax ...^@myorg/utils excludes the named workspace and includes only its dependents. This is useful when the named workspace has already been tested separately.


c. Git-based filtering and concurrency limits

The [<ref>] syntax filters by changes since a git reference. It selects workspaces whose files have changed since the reference.

# Run test only in workspaces changed since main
pnpm --filter "[origin/main]" run test

# Run test in changed workspaces and their dependencies
pnpm --filter "...[origin/main]" run test

This is the most important filter for CI pipelines. It reduces the execution set to the minimum required by the change. Combined with the ... suffix, it includes dependencies that might need rebuilding.

The --workspace-concurrency flag limits the number of simultaneous executions. The --parallel flag ignores topological order.

# Run tests with 2 concurrent workspaces
pnpm -r --workspace-concurrency=2 run test

# Run linting in all workspaces in parallel (no order)
pnpm -r --parallel run lint

The concurrency limit applies to -r (recursive) commands. Without it, the default is 4 for run, which may be too high for CI runners with limited memory.


Complete Example Session

# ============================================
# PART 1: LIST WORKSPACES
# ============================================

pnpm m ls
# @myorg/app
# @myorg/utils
# @myorg/ui
# @myorg/docs
# ============================================
# PART 2: FILTER BY NAME
# ============================================

pnpm --filter @myorg/app run build
# Runs only in the app workspace.
# ============================================
# PART 3: FILTER BY DIRECTORY
# ============================================

pnpm --filter "./apps/*" run test
# Runs in all workspaces under apps/.
# ============================================
# PART 4: EXCLUDE A WORKSPACE
# ============================================

pnpm --filter "!@myorg/docs" run test
# Runs in all workspaces except docs.
# ============================================
# PART 5: INCLUDE DEPENDENCIES
# ============================================

pnpm --filter @myorg/app... run build
# Builds app, ui, and utils in topological order.
# ============================================
# PART 6: INCLUDE DEPENDENTS
# ============================================

pnpm --filter ...@myorg/utils run test
# Tests utils, ui, and app.
# ============================================
# PART 7: GIT-BASED FILTER
# ============================================

pnpm --filter "...[origin/main]" run test
# Tests only workspaces changed since origin/main
# and their dependencies.
# ============================================
# PART 8: LIMIT CONCURRENCY
# ============================================

pnpm -r --workspace-concurrency=2 run test
# At most 2 workspaces run at a time.
# ============================================
# PART 9: PARALLEL EXECUTION
# ============================================

pnpm -r --parallel run lint
# All workspaces run simultaneously.

The nine parts trace the filter syntax: listing, name filter, directory filter, exclusion, dependency traversal, dependent traversal, git-based changes, concurrency limit, and parallel execution.


Quick Reference

Filter Syntax

PatternMeaning
--filter <name>Specific workspace
--filter "./apps/*"By directory
--filter "!<name>"Exclude
--filter <name>...Workspace and dependencies
--filter ...<name>Workspace and dependents
--filter "...[<ref>]"Changed since git ref + deps

Execution Flags

FlagPurpose
-r, --recursiveAll workspaces
--parallelIgnore topological order
--workspace-concurrency=NLimit simultaneous execution

Best Practices

✅ Do This:

pnpm --filter "...[origin/main]" run test
<!-- Test only changed workspaces in CI -->           # ✅
pnpm --filter @myorg/app... run build
<!-- Build dependencies before dependents -->         # ✅
pnpm -r --workspace-concurrency=2 run test
<!-- Limit CPU saturation on CI runners -->           # ✅

❌ Don’t Do This:

# Run all tests in all workspaces for every change
<!-- Wastes CI time and money -->                    # ❌
# Use --parallel for build tasks
<!-- Dependencies may build out of order -->         # ❌
# Omit concurrency limit on large monorepos
<!-- CPU starvation causes test timeouts -->         # ❌

Common Pitfalls

PitfallWhy It HappensFix
Filter matches nothingWrong name or directory patternVerify name in package.json
Build fails on dependencyNo traversal filterUse <name>...
Tests timeoutToo many concurrent workspacesLimit --workspace-concurrency
CI runs full suiteNo git-based filterUse "[origin/main]"
Parallel build fails--parallel used with dependenciesRemove --parallel, use topological

Real-World Examples

1. Build App and Dependencies

pnpm --filter @myorg/app... run build

2. Test Utils and Dependents

pnpm --filter ...@myorg/utils run test

3. Only Changed Workspaces

pnpm --filter "...[origin/main]" run test

4. Exclude Docs

pnpm --filter "!@myorg/docs" run lint

5. Limit Concurrency

pnpm -r --workspace-concurrency=2 run test

6. Parallel Lint

pnpm -r --parallel run lint

7. Filter by Directory

pnpm --filter "./apps/*" run test

8. Multiple Filters

pnpm --filter @myorg/app --filter @myorg/ui run build

9. Check Filter Result

pnpm --filter @myorg/app... list --depth -1

10. Git Changed + Dependencies

pnpm --filter "...[HEAD~1]" run build

Visual

Filter Traversal

┌──────────────────────────────────────────────┐
│  @myorg/app depends on:                      │
│  ├── @myorg/ui                               │
│  │   └── @myorg/utils                        │
│  └── @myorg/utils                            │
└──────────────────────────────────────────────┘

--filter @myorg/app...
→ @myorg/app, @myorg/ui, @myorg/utils (topological order)

--filter ...@myorg/utils
→ @myorg/utils, @myorg/ui, @myorg/app (topological order)

The ellipsis position determines the direction.

CI Optimization

┌──────────────────────────────────────────────┐
│  Full test run                               │
│  10 workspaces × 30 seconds = 5 minutes      │
└──────────────────┬───────────────────────────┘
                   │
                   │ --filter "...[origin/main]"
                   ▼
┌──────────────────────────────────────────────┐
│  Changed-only test run                       │
│  2 workspaces × 30 seconds = 1 minute        │
└──────────────────────────────────────────────┘

The filter reduces the execution set to what is actually affected.


Summary

ItemValue
Name filter--filter <name>
Directory filter--filter "./apps/*"
Exclude filter--filter "!<name>"
Dependencies--filter <name>...
Dependents--filter ...<name>
Git changes--filter "[<ref>]"
Concurrency limit--workspace-concurrency=N
Parallel execution--parallel

Key takeaways:

  • The --filter flag is the orchestration tool. It selects workspaces by name, directory, or git change. The selection determines what runs.
  • The ellipsis traverses dependencies. <name>... includes dependencies. ...<name> includes dependents. The direction matters for build vs test.
  • Git-based filters reduce CI time. "[origin/main]" selects only workspaces with changed files, cutting the execution set to what is actually affected.
  • --workspace-concurrency prevents CPU starvation. Running too many workspaces simultaneously degrades performance and causes timeouts.
  • --parallel is for independent tasks. It ignores topological order and runs everything at once. It is safe for linting and unsafe for builds.

Remember: Filtering is precision. --filter selects the target. ... adds dependencies or dependents. [ref] narrows to git changes. The combination determines the execution set. The flags --parallel and --workspace-concurrency determine how the set runs. The right configuration is the one that does the minimum work required by the change.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!