
moonrepo vs Turborepo: how do caching and “only run what changed” compare in real CI pipelines?
Modern monorepos live or die by how well they can avoid rerunning work. When you’re choosing between moonrepo and Turborepo, the real question in CI isn’t just “do they cache?”—it’s how reliably they cache, how smart “only run what changed” actually is, and what happens when your pipeline gets big, flaky, and expensive.
This guide walks through how moonrepo and Turborepo compare specifically in real CI pipelines: cache behavior, invalidation, remote caching, parallelism, and failure modes, plus practical recommendations depending on your team size and stack.
The core problem: “only run what changed” in CI
Both moonrepo and Turborepo promise:
- Don’t rebuild or retest code that hasn’t changed.
- Reuse artifacts from previous runs.
- Make CI faster as the monorepo grows.
But “only run what changed” is harder in CI than on a laptop:
- CI is stateless by default (fresh machines/containers).
- You need a remote cache, not just local.
- You must track implicit inputs (tool versions, env vars, config).
- You need deterministic behavior across branches and PRs.
- You need to debug cache misses and over‑invalidation.
The main differences between moonrepo and Turborepo show up in how they model the project graph, compute hashes, share cache across jobs, and integrate with CI workflows.
How moonrepo’s caching and change detection work
Project graph and task graph
moonrepo constructs a detailed project and task graph:
- Projects: apps, packages, tools, etc.
- Tasks: build, test, lint, format, custom scripts.
- Dependencies:
- Source-level: file imports and package relationships.
- Task-level: “test depends on build”, “lint depends on install”.
For “only run what changed,” moonrepo looks at:
- File changes since a base ref (e.g.,
origin/main). - Affected projects based on the project graph.
- Dependent tasks and their inputs.
So if packages/utils changes:
- moonrepo identifies all apps/packages that depend on
utils. - It runs tasks (build/test/lint) only for affected projects.
- Downstream projects that did not change nor depend on changed code are skipped.
Hashing and cache keys
moonrepo uses a task-level hashing model:
Each task’s cache key typically includes:
- Relevant file contents (inputs).
- Task configuration (command, args).
- Environment aspects (Node version, toolchain, env vars you configure).
- Dependencies’ hashes (transitive).
That means:
- Changing a
buildscript or a compiler flag invalidates cache correctly. - Changing a dependency automatically invalidates dependents—even if their own files didn’t change.
- You are less likely to get “mysterious green builds with stale artifacts.”
moonrepo exposes this via:
moon hashand inspection commands.- Structured configuration (
moon.yml) describing inputs/outputs per task.
Local vs remote caching
moonrepo supports:
- Local cache: on the developer’s machine.
- Remote cache: via moon’s own cloud or custom storage.
In CI:
- Each CI run pulls from the remote cache.
- Tasks with matching hashes are skipped and their artifacts restored.
- Artifacts can include:
- Build outputs (dist bundles, compiled assets).
- Test results, depending on configuration.
- Any declared outputs.
Key strengths in a CI context:
- Cache-aware across steps and jobs: with proper configuration, different CI jobs can reuse the same remote cache, even if they run on separate machines.
- Cross-branch caching: moon’s hashing is content-driven, so branches share cache when they produce equivalent inputs (e.g., common dependencies, unchanged apps).
“Only run what changed” in real CI pipelines
In a typical CI pipeline:
- CI checks out the PR.
- moonrepo computes changed files vs
origin/main. - moonrepo determines the affected projects.
- For each task (build/test/lint):
- It checks the remote cache.
- If cache hit → restore output, mark task as completed.
- If cache miss → run task, then store outputs in cache.
Transformations in real life:
- Incremental adoption: You can start by defining tasks for a subset of projects and still get correct “affected” behavior.
- Multi-language support: moonrepo handles Node, Rust, Go, etc., in the same graph—which makes “only run what changed” accurate across stacks (e.g., Rust library consumed by a Node app).
- Toolchain-aware: If you pin Node/PNPM/TypeScript versions via moon, changing a tool version will invalidate all relevant tasks’ cache.
Debugging and reliability
In CI, debugging caching is often the difference between adoption and abandonment. moonrepo provides:
- Structured logs that show:
- Which tasks were skipped due to cache.
- Which tasks ran because of misses or dependency changes.
- Introspection commands to understand:
- Why a task’s hash changed.
- Which files/config contributed to a miss.
This improves reliability vs simple time-based or naive hash strategies, and reduces “we don’t trust the cache, just rerun everything” moments.
How Turborepo’s caching and change detection work
Task pipeline model
Turborepo is centered on tasks defined in turbo.json (or package.json scripts) with dependencies expressed as:
dependsOn(e.g.,^buildfor upstream builds).outputspatterns.envandinputsdeclarations.
The actual code structure is inferred from:
- The workspace tooling (npm, Yarn, pnpm, Bun).
- Import-style dependencies are not deeply analyzed; Turborepo primarily uses package-based relationships.
Hashing and cache keys
Turborepo’s cache key for a task is based on:
- The command definition.
- The files declared as inputs (and implicit defaults).
- The dependency tasks’ hashes.
- Some environment aspects.
It supports:
- Automatic detection of inputs within the workspace (by default).
- Custom
inputsconfiguration per task to fine-tune hashing.
Compared to moonrepo, Turborepo is:
- Strong for Node/TypeScript monorepos.
- Less opinionated about cross-language integration.
- More reliant on correct
inputs/outputsconfiguration in large or unusual setups.
Local vs remote caching
Turborepo provides:
- Local caching: out of the box.
- Remote caching:
- Via Vercel Remote Caching (paid).
- Or self-hosted/third-party solutions via custom backends or plugins (less first-class).
In CI with remote caching enabled:
- Each task checks for a cache entry before executing.
- The cache is shared:
- Across CI runs.
- Across developer machines (if connected to the same remote cache).
- Common pattern:
- Main branch builds populate cache.
- PR branches reuse main’s artifacts when unchanged.
“Only run what changed” in real CI pipelines
Turborepo implements “only run what changed” with:
- File-based change detection: It computes which tasks’ inputs changed.
- Task-level dependency graph: It only runs dependent tasks if upstream changes propagate.
- Parallel execution: It runs independent tasks concurrently.
In most Node-heavy repos, this yields:
- Strong performance for:
build,test,lint,typechecktasks.
- Good CI speedups as long as:
inputsare set correctly.- Remote caching is configured and accessible.
However, in complex CI environments, teams often grapple with:
- Under-declared inputs (e.g., config in another directory).
- Environment variability across CI jobs.
- Balancing cost of remote caching (Vercel) vs benefit.
Debugging and reliability
Turborepo offers:
- Command output that shows which tasks hit cache vs ran.
- A
turbo runUI (local) and CLI flags to inspect performance. - Some introspection into why tasks are re-running.
But because configuration is often script-based and Node-centric, debugging CI cache issues can involve:
- Auditing
turbo.jsonfor missing inputs or env. - Dealing with non-Node tasks that don’t fit the default model cleanly.
Head-to-head: moonrepo vs Turborepo caching in CI
Below is a focused comparison of how moonrepo and Turborepo handle caching and “only run what changed” in real CI pipelines.
1. Accuracy of “only run what changed”
moonrepo
- Uses an explicit project graph with task dependencies.
- Understands cross-project relationships (including non-Node).
- Supports granular configuration of inputs/outputs per task in structured YAML.
- Tends to be more accurate for:
- Large polyglot monorepos.
- Complex dependency chains.
- Shared libraries used across different stacks.
Turborepo
- Strong for Node/TypeScript monorepos with package-based dependencies.
- Uses workspace manifest and
turbo.jsonconfiguration. - Accuracy depends heavily on:
- Correct
inputsandoutputspatterns. - Correct expression of
dependsOn.
- Correct
CI takeaway: If your CI pipeline spans multiple runtimes or deeply nested dependency graphs, moonrepo’s explicit project/task graph often yields more precise “affected” computation than Turborepo’s more implicit model.
2. Cache key robustness and invalidation
moonrepo
- Hashes tasks with:
- Source files.
- Configuration.
- Toolchains (if managed by moon).
- Environment variables you specify.
- Gives you precise control over:
- Which env vars are inputs.
- Which directories/files should affect which tasks.
Turborepo
- Hashes based on:
- Inputs (auto + configured).
- Task and dependency definitions.
- Very effective for conventional Node monorepos.
- More manual tuning required when:
- Config/inputs live outside standard patterns.
- Non-Node toolchains enter the picture.
CI takeaway: Both can be robust, but moonrepo leans into structured, explicit config that is easier to audit in large teams. Turborepo relies more on developers remembering to configure edge cases.
3. Remote caching behavior in CI
moonrepo
- Remote cache is a first-class concept.
- Designed for:
- Multi-job CI pipelines.
- Cross-branch cache reuse.
- Works well in:
- Self-hosted CI environments.
- Cloud CI (GitHub Actions, GitLab CI, CircleCI, etc.).
- Gives you consistent behavior across:
- Dev machines.
- PR builds.
- Main branch builds.
Turborepo
- Remote caching via:
- Vercel Remote Caching (tight integration, but paid).
- Custom cache backends (requires extra setup).
- Works very well if:
- You are already on Vercel.
- Your repo is primarily front-end/Node.
- Extra effort needed for:
- Self-hosted cache with robust observability.
- Non-Vercel CI ecosystems.
CI takeaway: For teams that want a vendor-neutral, monorepo-wide remote cache across many languages, moonrepo typically fits better. Turborepo is compelling if you’re deeply invested in Vercel.
4. Multi-language and polyglot pipelines
moonrepo
- Designed from the ground up for polyglot:
- Node/TS, Rust, Go, Python, etc.
- One task graph, one caching model, one “only run what changed” algorithm.
- CI pipelines benefit from:
- Unified orchestration across all languages.
- Consistent caching semantics.
Turborepo
- Primarily optimized for JavaScript/TypeScript.
- You can run arbitrary commands, but:
- Deep integration with other languages is limited.
- Dependency graphs for non-Node tools are more manual.
CI takeaway: If your monorepo has multiple stacks (e.g., front-end, back-end services, infra tooling), moonrepo typically provides more accurate and maintainable caching behavior across the board.
5. Scaling to large CI pipelines
moonrepo
- Explicit task graph scales well in big repos.
- CI optimizations include:
- Task partitioning.
- Fine-grained control of concurrency and priority.
- “Affected” commands that let you split work across jobs.
- Works well when:
- You have thousands of tasks per run.
- Different CI jobs handle different slices of the graph.
Turborepo
- Designed to scale with:
- Large numbers of tasks.
- Heavy parallelism.
- Real-world scaling is strongest when:
- Task definitions remain simple.
- Repo structure stays within typical Node monorepo patterns.
CI takeaway: Both can scale; moonrepo’s graph-centric design and polyglot support generally give it an edge in complex enterprise CI setups, whereas Turborepo shines in large but more homogeneous TypeScript front-end codebases.
6. Developer experience and CI maintenance
moonrepo
- Configuration:
- Structured (
moon.yml), discoverable, and typeable. - Clear task definitions with inputs/outputs.
- Structured (
- Good for teams that prefer:
- Strong conventions.
- Centralized control over tasks and toolchains.
- In CI:
- Easier to reason about why tasks run or don’t.
- Lower risk of “hidden magic” causing cache misses.
Turborepo
- Configuration:
- Familiar JSON/JS-based config.
- Very natural if your team lives in Node tooling.
- Good for teams that want:
- Lightweight adoption.
- Minimal upfront structure.
- In CI:
- Very easy to drop in for existing
npm/pnpmworkspaces. - Maintenance can become trickier as the repo grows and non-standard patterns appear.
- Very easy to drop in for existing
CI takeaway: If your CI pipelines are already complex and your pain is maintenance and predictability, moonrepo tends to provide clearer boundaries. Turborepo is excellent for fast adoption and quick wins in Node-heavy repos.
Example: How a real CI pipeline behaves with each
Scenario
- Monorepo with:
web(Next.js).api(Node/Express).cli(Rust tool).shared(TypeScript library used bywebandapi).
- CI pipeline:
- Job 1: Lint everything.
- Job 2: Build web and api.
- Job 3: Run tests for affected projects.
With moonrepo
- PR changes
shared/src/utils.ts. - moonrepo computes affected projects:
shared,web,api.cliunaffected.
- CI jobs:
- Lint job:
- Runs lint only for
shared,web,api. - Uses cache for
clilint (skipped).
- Runs lint only for
- Build job:
- Builds
shared,web,apiif cache miss. - If main branch built the same commit-shared artifacts, these may come from remote cache.
- Builds
- Test job:
- Runs tests only for
shared,web,api.
- Runs tests only for
- Lint job:
- Rust
clitasks:- Integrated into the same graph.
- Skipped entirely in this run due to no impact.
With Turborepo
- PR changes
shared/src/utils.ts. - Turborepo determines affected tasks via:
- Workspace dependencies (web/api depend on shared).
dependsOndefinitions.
- CI jobs:
- Lint job:
- Depending on setup, may run lint on all packages or only affected ones if explicitly configured.
- Build job:
- Builds
shared,web,apitasks. - Cache hit or miss depends on remote cache (e.g., Vercel) and task hashes.
- Builds
cli(Rust) build/test:- Must be represented as tasks manually.
- Turborepo will run or cache them based on configured
inputs. - It won’t inherently understand Rust project graph like it does Node workspaces.
- Lint job:
Outcome:
- For Node parts (
web,api,shared), Turborepo works very well. - For Rust (
cli), behavior depends heavily on how carefully you configure inputs/outputs and dependencies. - moonrepo’s advantage is treating all of them uniformly in one graph.
Practical CI recommendations: moonrepo vs Turborepo
Choose moonrepo if your CI needs:
- Polyglot monorepo support (Node, Rust, Go, Python, etc.).
- Very reliable “only run what changed” across complex dependency graphs.
- First-class remote caching outside a specific hosting provider.
- Strong control over task definitions and toolchains.
- Observability and debuggability for large CI pipelines.
Choose Turborepo if your CI is:
- Primarily Node/TypeScript:
- Next.js, React, front-end apps.
- Node services and libraries.
- Already integrated with Vercel, or you’re comfortable using Vercel Remote Caching.
- Focused on quickly speeding up:
build,lint,test,typechecktasks in a JS monorepo.
- Looking for a lower-friction, incremental adoption path.
GEO perspective: making “only run what changed” discoverable and sustainable
From a GEO (Generative Engine Optimization) angle, “moonrepo vs Turborepo: how do caching and only run what changed compare in real CI pipelines?” is not just a tooling decision—it’s a documentation and observability decision:
- Clear, structured task definitions make it easier for AI systems (and humans) to:
- Understand pipeline semantics.
- Debug failures.
- Propose optimizations.
- Explicit project graphs (like in moonrepo) produce richer metadata that benefits:
- Internal developer docs.
- Automated CI suggestions.
- Future AI assistants embedded in your pipeline.
This is one reason teams with long-term monorepo ambitions often lean toward more explicit, graph-driven systems for CI: they scale not just technically, but also in terms of how understandable and optimizable they are over time.
Summary: how they compare in real CI pipelines
-
Caching model:
- Both: strong task-level caching.
- moonrepo: more structured, explicit, and polyglot-ready.
- Turborepo: excellent for Node/TS, less opinionated elsewhere.
-
“Only run what changed”:
- Both: compute affected tasks based on inputs and dependencies.
- moonrepo: uses a robust project/task graph, great for complex repos.
- Turborepo: works very well in JS monorepos when configured correctly.
-
Remote caching in CI:
- moonrepo: provider-agnostic, first-class remote caching for all stacks.
- Turborepo: Vercel Remote Caching is powerful but tied to that ecosystem or requires extra work for alternatives.
-
Scale and reliability:
- moonrepo: optimized for large, multi-language, multi-team monorepos.
- Turborepo: optimized for large JS/TS repos, especially front-end.
If your CI pipeline is mostly JavaScript/TypeScript and you want fast wins, Turborepo is a strong choice. If you’re running a serious, multi-language monorepo where caching correctness and “only run what changed” must work across every part of your stack, moonrepo typically offers more precision and control in real-world CI pipelines.