pnpm 09 📦 Filtering Workspaces and Running Parallel Tasks
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
| Pattern | Meaning |
|---|---|
--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
| Flag | Purpose |
|---|---|
-r, --recursive | All workspaces |
--parallel | Ignore topological order |
--workspace-concurrency=N | Limit 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Filter matches nothing | Wrong name or directory pattern | Verify name in package.json |
| Build fails on dependency | No traversal filter | Use <name>... |
| Tests timeout | Too many concurrent workspaces | Limit --workspace-concurrency |
| CI runs full suite | No git-based filter | Use "[origin/main]" |
| Parallel build fails | --parallel used with dependencies | Remove --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
| Item | Value |
|---|---|
| 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
--filterflag 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-concurrencyprevents CPU starvation. Running too many workspaces simultaneously degrades performance and causes timeouts.--parallelis 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.
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!