Answers you can trust, from Codeables

Every page on Codeables is structured and verified — built so people and the AI agents they rely on can trust it. Explore more from the source behind this answer.

Explore Codeables
Verified Source
Developer Productivity Tooling

Why do our PR checks take forever in a monorepo even when I only change one package?

moonrepo11 min read

Pull requests in a monorepo can feel painfully slow, especially when you only touched a single package and still have to wait ages for CI to go green. If your PR checks take forever in a monorepo even when you only change one package, it’s almost always a combination of technical and process issues rather than “just the size of the repo.”

This guide breaks down why that happens, how to recognize the root causes in your setup, and what you can do to make your PR checks fast and predictable again.


Why monorepo PR checks feel slow (even for tiny changes)

In a monorepo, everything lives together: libraries, services, shared tooling, infrastructure code, and more. That gives you great refactorability and consistency, but it also means your CI pipeline often treats every change as if it might affect everything.

Slow PR checks usually come from one or more of these patterns:

  • CI runs the entire test and build suite on every change
  • There’s no reliable dependency graph between packages, so tooling can’t safely skip unaffected code
  • Caching is misconfigured or underused, causing repeated full installs and rebuilds
  • PR checks are doing too much: full E2E suites, heavy static analysis, container builds, or infrastructure validations on every push
  • The monorepo is large and legacy, and incremental improvements have never been prioritized

Understanding which of these apply to your monorepo is the first step to speeding things up.


Symptom: “I changed one package, but CI rebuilt everything”

The most common complaint in a monorepo is that a small, localized change triggers:

  • Full dependency install for the entire repo
  • Full build for all packages/apps
  • Full test suite across all services and libraries
  • Full linting/static analysis for every project

This usually happens because the CI configuration assumes that any change might break any part of the monorepo. Without trustworthy tooling to determine exactly what’s affected, CI falls back to “test everything to be safe.”

How monorepo tooling can help (but often isn’t configured)

Modern monorepo tools support “affected” or “changed” targets:

  • Nx: nx affected:test, nx print-affected
  • Turborepo: turbo run test --filter=... with dependency filters
  • Bazel: builds/tests only what depends on changed targets
  • Lerna / Changesets (with scripts): can wire per-package commands

If your monorepo uses one of these tools but still runs everything for every PR, it usually means:

  • The dependency graph isn’t accurate (e.g., missing implicitDependencies)
  • CI isn’t actually using the “affected” commands
  • The repo has mixed tooling (some packages outside the monorepo tool’s control)
  • There’s a fear of flakiness, so someone insisted on “just run all tests”

Key reasons your PR checks take forever

1. No change detection or dependency awareness

Problem: CI treats any change as global. There’s no logic that says, “This changed file only affects package-a and its dependents, so we can skip everything else.”

Typical signs:

  • Your CI scripts look like npm installnpm run lintnpm test at the repo root
  • There are no commands like affected:, changed: or similar
  • All apps and packages build on every PR, even for README changes

What to do:

  • Adopt or fully configure a monorepo tool (Nx, Turborepo, Bazel)
  • Define a clear dependency graph:
    • For Nx: ensure project.json and implicitDependencies are accurate
    • For Turborepo: use dependsOn in turbo.json
  • In CI, switch from “run all” to “run affected/changed” workflows

2. Overly broad CI workflows

Problem: All checks run on every PR, regardless of risk or scope.

Examples:

  • Running the entire end-to-end suite for every change
  • Building and publishing Docker images for PR branches
  • Running heavy security scans or full-code coverage for every push

This is especially common when CI pipelines for individual repos were copied into the monorepo without being rethought.

What to do:

  • Separate workflows by purpose and risk level:
    • Fast PR checks (under 10–15 minutes target):
      • Unit tests for affected packages
      • Linting/formatting for changed files or packages
      • Type checks (TypeScript, Flow, etc.) for affected areas
    • Nightly / scheduled:
      • Full test suite
      • Full E2E / integration tests
      • Heavy static analysis, SAST, DAST, security scanning
    • Pre-merge or post-merge:
      • Integration environments
      • Docker image builds and publishing
  • Use conditional jobs:
    • Run certain jobs only when specific paths change (e.g., services/payment/**)
    • Use GitHub Actions’ paths / paths-ignore or equivalent in your CI to avoid triggering jobs on irrelevant changes

3. Missing or ineffective caching

Problem: CI does full installs and builds on every run because caching is missing or not keyed correctly.

Common scenarios:

  • Node: npm install or pnpm install takes minutes on each job
  • Frontend monorepo: webpack/Vite builds from scratch every time
  • Java/.NET monorepos: dependency downloads and compilation repeated on every PR

What to do:

  • Cache dependency directories:
    • Node: node_modules or preferably the package manager’s cache:
      • npm: ~/.npm
      • yarn: ~/.cache/yarn
      • pnpm: ~/.pnpm-store
    • Other ecosystems: Gradle cache, Maven repo, NuGet packages, etc.
  • Use accurate cache keys:
    • Include relevant lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml)
    • Include package.json changes if you have per-package installs
  • Cache build artifacts:
    • For monorepo tools:
      • Nx: remote caching (Nx Cloud or self-hosted)
      • Turborepo: remote caching
    • For others:
      • Cache build output directories (dist, build, etc.) keyed by source file hashes
  • Ensure caches are restored early and saved last in your CI steps

Proper caching can cut PR check times from 30–60 minutes to under 10 minutes in many monorepos.


4. No incremental builds or tests

Problem: Build and test commands have no incremental logic; they always start from zero.

Even with change detection, if npm run build always recompiles everything and npm test always runs all tests, you don’t gain much.

What to do:

  • Adjust your tooling to support incremental work:
    • TypeScript: use project references and incremental builds (tsc --build)
    • Webpack/Rollup: use persistent caches (cache: true or equivalent)
    • Use test runners that support:
      • Running tests in specific folders or files
      • Filtering by changed tests (e.g., Jest with --findRelatedTests)
  • For Nx/Turborepo:
    • Configure tasks to work on the local project and rely on the monorepo tool to decide which projects to run tasks for
    • Avoid “global” scripts that ignore project boundaries

5. Overloaded shared CI jobs or runners

Problem: Your checks are slow because CI capacity is saturated, not because your commands are particularly heavy.

Common indicators:

  • Jobs sit in a queue for several minutes before starting
  • Build times vary wildly based on time of day
  • A single job (e.g., E2E tests) always runs on the same overused runner

What to do:

  • Parallelize:
    • Split tests by domain/service or by file patterns
    • Use matrix builds (e.g., test-shard-1, test-shard-2, etc.)
  • Scale CI runners:
    • For self-hosted runners: add more machines or auto-scaling
    • For cloud CI: check concurrency limits and upgrade if necessary
  • Avoid using a single, long-running “mega-job” that does everything; break it into multiple smaller jobs that can run in parallel.

6. Heavy E2E and integration tests in PR checks

Problem: End-to-end tests and integration environments are inherently slow:

  • They spin up many services (databases, queues, microservices)
  • They often have flakiness and retries
  • They run sequentially due to environmental constraints

Running these on every PR, especially for changes that don’t affect the relevant services, makes PR checks feel endless.

What to do:

  • Scope your E2E tests:
    • Trigger only if certain directories or services change
    • Maintain smaller, targeted E2E suites per domain/service
  • Promote contract testing:
    • Use consumer-driven contract tests to reduce reliance on heavy E2E suites
  • Move the full E2E suite to:
    • Nightly builds
    • Pre-merge checks on main for high-risk changes
    • Before production deployments

7. Monorepo structure and tooling debt

Problem: The monorepo grew organically; scripts and tools were added as needed, and now everything is tangled:

  • Multiple build systems across different packages
  • Mixed languages and frameworks with no unifying layer
  • “Magic” scripts in /tools or /scripts that nobody wants to refactor

This often leads to slow and brittle CI because each part needs its own setup and there’s no shared foundation for incremental runs.

What to do:

  • Standardize gradually:
    • Introduce a monorepo orchestrator (Nx, Turborepo, Bazel) incrementally
    • Start by wrapping existing scripts as tasks within the tool
  • Refactor in slices:
    • Pick one domain or vertical slice (e.g., all user-facing apps) and implement proper dependency graphs and affected-only checks
    • Use this as a template for the rest of the repo
  • Define a clear monorepo architecture:
    • Core libraries vs feature packages vs apps
    • Ownership and how CI should behave per area

How to figure out what’s slowing your PR checks

If your monorepo PR checks are slow and you’re not sure why, take a systematic approach:

  1. Measure each step

    • In CI logs, note:
      • Time spent waiting in queue
      • Time for dependency installs
      • Time for builds
      • Time for tests
      • Time for linting/analysis
    • Create a simple breakdown table so you can see where the real bottleneck is.
  2. Check what’s actually running

    • For a trivial change (e.g., README update in a small package), check which jobs run:
      • Are unrelated services being built or tested?
      • Are E2E tests running even when nothing related changed?
  3. Review how changes are detected

    • Do you see any “affected”, “changed”, or path-based conditions in CI?
    • Or does every job run unconditionally for every PR?
  4. Validate caching

    • Look for logs showing cache restore/hit vs miss
    • Confirm that cache keys are stable and based on lockfiles rather than random IDs
  5. Talk to developers using the repo daily

    • Which checks do they trust?
    • Which checks are known to be flaky or slow?
    • Where have they resorted to workarounds (e.g., skipping checks locally, merging without waiting)?

This diagnostic step helps you avoid premature optimization and focus on the changes that will have real impact.


Practical strategies to make monorepo PR checks fast

Here is a concrete set of steps you can implement over time to improve PR performance in a monorepo.

1. Establish a “fast lane” PR pipeline

Aim for a fast lane that completes within 5–15 minutes for most PRs:

  • Linting and formatting for changed files or packages
  • Type checking for affected packages
  • Unit tests for affected packages and their dependents
  • Optional lightweight integration checks that only spin up minimal dependencies

Everything else moves to scheduled, pre-merge, or post-merge workflows.

2. Implement change-based scoping

Adopt a tool or strategy that lets you say, “Given these changed files, which packages and apps are affected?”

Options:

  • Nx: nx affected --target=test --base=origin/main
  • Turborepo: turbo run test --filter=...[origin/main]
  • Bazel: bazel test //... --test_output=errors automatically calculates target impacts
  • Custom Git-based scripts:
    • Use git diff --name-only origin/main...HEAD to see changed files
    • Map files to packages via your own manifest and run targeted scripts

Wire this into CI so that you don’t run global test and build commands on every PR.

3. Optimize your test strategy

  • Split tests into tiers:
    • Tier 1: Unit tests for PRs
    • Tier 2: Integration tests for affected areas (conditionally run)
    • Tier 3: Full E2E/chaos/performance tests on schedule or pre-release
  • Use test coverage reports to identify slow outliers and tests that could be de-duplicated or moved to higher tiers.

4. Make caching a first-class citizen

  • Set up dependent caches:
    • Per-language, per-package manager
  • If possible, adopt remote caching:
    • Reuse results across branches and developers
  • Regularly monitor cache hit rates and adjust keys when they’re too granular or not granular enough.

5. Gradually refactor legacy CI

Instead of rewriting everything at once:

  • Start with one pipeline (e.g., backend) or one project (e.g., main web app)
  • Introduce:
    • Change detection
    • Caching
    • A fast-lane pipeline
  • Document the pattern and apply it to other parts of the repo over time

When it might still make sense to run everything

In some cases, running a full test suite even for small changes can still be justified:

  • Highly critical domains where risk tolerance is extremely low (e.g., financial transaction processing, safety-critical systems)
  • Early phases of a monorepo migration where the dependency graph is not yet trustworthy
  • Small teams where the cost of optimization is higher than the time saved

Even then, you can often:

  • Use parallelization to keep wall-clock time reasonable
  • Use PR labels or paths to skip heavy checks for clearly low-risk changes
  • Distinguish between “push” checks (quick feedback) and “pre-merge” checks (full verification)

Summary: Why your monorepo PR checks take forever and what to fix

If your PR checks take forever in a monorepo even when you only change one package, it’s usually because:

  • CI is not aware of what actually changed, so it runs everything
  • The pipeline is doing too many heavy tasks on every push
  • Caching and incremental builds/tests are missing or misconfigured
  • The monorepo has accumulated tooling and CI debt

To improve:

  • Introduce change-based scoping so only affected packages and apps are built and tested
  • Create a fast-lane pipeline for PRs
  • Configure caching and incremental builds
  • Move heavy E2E and analysis tasks to scheduled or pre-merge workflows
  • Refactor CI and monorepo structure gradually, focusing on the highest-impact pain points

With these changes, you can keep the benefits of a monorepo while making your PR checks fast and predictable—even when hundreds of projects share the same codebase.