Copy link to headingWhy Monorepos Succeed or Fail for Full-Stack Teams
Most teams pick a monorepo for the right reasons and then run it the wrong way. They move everything into one repository, skip the build infrastructure that makes it viable, and end up slower than before. They're paying the coordination costs of one big repo without gaining anything that a monorepo is actually supposed to give you.
We run our own product as a monorepo powered by Turborepo, and we maintain the build system thousands of other teams use. What separates teams that thrive in a monorepo from those that struggle is almost never the directory layout. It's whether the build system knows what changed and only runs work that needs to run. For full-stack JavaScript and TypeScript teams, the monorepo is the right default, but only if you invest in the build layer from day one.
Key takeaways
Full-stack JS/TS teams that share UI and application code get the biggest return from a monorepo: every shared change becomes a single atomic commit instead of a coordinated release across repos.
Monorepo CI costs grow linearly with package count, independent of per-package complexity, and the build system is the only thing that arrests that growth.
Remote Caching determines whether a monorepo pays off over time: teams save an average of 76.8 hours per month once it's correctly configured.
A monorepo without incremental builds and a shared cache is strictly worse than a polyrepo, not equivalent to one.
The monorepo default breaks down for polyglot stacks and teams whose infra investment is outpacing dev capacity. Those are the clearest signals to reconsider.
Copy link to headingTreat the shared component as your unit of decision
One question cuts through most of the monorepo debate: how much does it cost your team to update a shared component?
In a polyrepo, that cost is fixed and repeating. The change size doesn't matter. You write the change, open a PR in the shared library, wait for review, publish a new npm version, then open separate PRs in each downstream repo to bump the dependency, and wait for each to land. A two-line bug fix in a shared button component can touch six repositories before it reaches production.
A monorepo makes a single commit. The change, the downstream consumers, and the rollout ship together in one PR. That's what monorepos are genuinely good at: keeping shared code in sync across apps without the coordination overhead of separate release cycles.
Whether a monorepo makes sense for your team follows directly from that coordination cost. If your team maintains a design system used by multiple apps, or shares utilities and types across a frontend and backend, the cost is real, and a monorepo removes it. If your services share nothing, that cost never appears, and a monorepo just adds complexity. Team size and engineer count don't change the answer. The shared component does.
When in doubt, start with a monorepo. If the structure doesn't fit, you'll see it early.
Copy link to headingExpect your CI bill to grow mechanically, not just proportionally
Adding packages to a monorepo increases CI work even if none of the individual packages get harder to build. A repo that starts with three packages and reaches fifty by month six now runs install, lint, typecheck, build, and test across all fifty for every push, regardless of what changed. The per-package work is the same. There are just sixteen times more of it.
We've watched that growth compound across teams at different scales. Samsara hit it directly: frontend tests, Go tests, Python tests, linters, and build checks all ran in a single pipeline, until engineers were retrying failed CI runs unrelated to their change. A build system that can't trace which packages were actually affected runs every task on every push. The cost grows with the repo, not with the complexity of the work.
Turborepo addresses the every-task-on-every-push problem by building a task graph across all packages and running only what changed, in parallel, across all available cores. It caches task outputs, so an unchanged package is never rebuilt. Working with AI agents in Vercel Sandboxes, we increased turbo run execution speed by up to 96%. The goal is to prevent the repo's package count from appearing as a line item in every PR.
The runaway CI cost compounds slowly, which makes it easy to miss. By the time a fifty-package repo feels slow, that slowness is the new baseline. The build system needs to be in place before the repo grows into it.
Copy link to headingCache the work, not just the repo
Turborepo hashes each task's inputs: the package config, lockfile state, and source files. If the hash matches a previous run, it restores the cached output instead of running the task again. On Vercel, the cache is shared across your whole team and CI pipeline, so work one engineer already did doesn't have to be repeated for anyone else.
Since Turborepo joined Vercel, teams have saved an average of 76.8 hours per month through remote caching. By the Turborepo 2.0 release, the shared cache had saved 347 years of compute time across all teams using it. On December 20, 2024, we made Remote Cache free on all plans, including for teams not hosting on Vercel.
Results across teams of different sizes:
Makeswift cut CI pipeline time by 65% after adopting Turborepo, moving off a multi-monorepo setup managed with GitHub Actions and Docker builds.
Andrew Lisowski at Descript: Turborepo cut their CI bill in half, saving $20k.
Matt Pocock at Stately AI: Turborepo saved 67 hours of CI since adoption.
The Next.js core team cut publish times by 80% after implementing Remote Caching.
The 80% improvement in Next.js took real work to achieve. The team built custom scripts for FreeBSD builds, tuned cache invalidation so that version-only package changes didn't bust the cache, and deliberately configured their Remote Cache setup. Turning on caching is the starting point. Getting the full benefit requires knowing what invalidates your cache and why.
There's also a size consideration. Turborepo's hashes are small, but the artifacts they represent can be large. A full Docker container is a good example: uploading and downloading from a remote cache often takes longer than rebuilding locally. Caching pays off when transferring the output is faster than producing it. Check your artifact sizes before assuming the cache is always the right path.
Copy link to headingKnow when a monorepo is the wrong answer
The monorepo default holds for full-stack JS/TS teams that share code. It breaks down in specific conditions that are worth naming before you commit.
Every repo structure comes with a different set of ongoing costs. Monorepos make coordination cheap and scaling expensive. Polyrepos make scaling cheap and coordination expensive. The question is which cost fits your team better.
Copy link to headingBuild the tooling before the repo punishes you
The most common way a monorepo fails isn't due to a wrong structural decision. It's the right structural decision, but there's no follow-through on the build system.
A team moves everything into one repo but keeps separate CI scripts, inconsistent configs, and no shared caching. The result is the scale problems of a large codebase without the coordination benefits that justified the move. A monorepo should not require you to rebuild everything on every change. When it does, the build graph isn't configured correctly. The directory layout isn't the problem.
Plan to spend real time on monorepo tooling: wiring up configs, writing scripts to handle edge cases, and getting cache invalidation right. Teams that skip this work don't avoid it. They defer it until the repo is large and every engineer feels the slowness.
Caching also has failure modes worth knowing. Tasks that complete faster than a remote cache round-trip get slower with caching, not faster. Very large artifacts, like a full Docker container, can cost more in transfer time than they save in build time. Tasks that have their own internal caching can interfere with Turborepo's cache in ways that are hard to debug. Check for these before assuming caching is solving more than it is.
Copy link to headingAudit Turborepo's fit before committing to it
Turborepo is built for JavaScript and TypeScript. That's the whole scope.
If your monorepo includes Go, Python, or Rust alongside JS, Turborepo handles the JavaScript packages, and you'll need a separate tool for the other languages. Running two build systems means two caching setups and two sets of failure modes side by side. The monorepo puts everything in one repository. It didn't give you a unified build.
Three cases where Turborepo's caching costs more than it saves:
Tasks that finish faster than a round-trip to the remote cache get slower with caching, not faster.
Artifacts that are cheaper to rebuild locally than to transfer will cost more in upload and download time than they save.
Tasks with their own internal caching often conflict with Turborepo's cache, making neither layer reliable.
Polyglot teams should plan explicitly for what Turborepo won't cover. The gap doesn't close itself.
Copy link to headingDeploy each app as its own Vercel project
On Vercel, a monorepo deployment works by connecting multiple projects to a single repository. Each project targets a specific directory and builds independently. One commit to the repo triggers a parallel build for each affected project.
Vercel detects which projects were affected by a push and skips builds for unchanged projects, without using a concurrent build slot for them. Affected-project detection runs before the Ignored Build Step, which matters: a build skipped at this stage doesn't consume capacity the way a build that starts and then cancels does. Add npx turbo-ignore to your ignoreCommand, and Turborepo's dependency graph handles detection of affected files, including transitive dependencies.
Two features handle the cross-project wiring that most often breaks. Related Projects links a frontend's preview deployment to the corresponding backend preview, so preview environments stay consistent without hardcoding URLs. Sidecar tasks in Turborepo 2.5 keep a web app and its backend API running together during local development.
Copy link to headingAccount for Skew Protection's cache miss before you ship
Skew Protection prevents a class of production breakage that happens during rolling deployments: a user's browser is on an older version of your frontend while your servers have already moved to a newer one, and the mismatched contract causes failures. Vercel handles this by binding each client request to the deployment that served it. The deployment ID accompanies the request, and the server routes it to the corresponding backend version. This is on by default for Next.js and SvelteKit projects created after November 19, 2024 on Pro and Enterprise plans.
Skew Protection directly conflicts with Turborepo's build cache. The deployment ID is injected as an environment variable at build time, and environment variables are part of the task hash. The ID changes on every deploy, so the hash does too. Every build of a Skew-Protected project will miss the Turborepo cache on the application build step.
Skew Protection and Turborepo caching are both worth keeping on, and on the build step they work against each other. Set your timing expectations for the build step accordingly, and don't treat the cache miss as a misconfiguration. It's the correct behavior for the tradeoff you've made.
Copy link to headingRun this checklist before you commit to a structure
Run through these conditions against your actual situation before settling on a monorepo or staying with a polyrepo. Several pointing in different directions is worth understanding before you commit.
You share JS/TS code across two or more apps. The coordination cost is real. A monorepo is the right default.
Your stack is polyglot. Turborepo won't cover your non-JS packages. Plan for a second build tool or keep those packages in separate repos.
You haven't planned CI caching investment for day one. A monorepo without a shared cache will be slower than the polyrepo you're leaving. Get the caching in place first.
Your infra team is growing faster than your dev team. The monorepo may be consuming more than it's returning. Evaluate before adding more to it.
Your fastest-changing packages are the ones you share most. The monorepo pays off most here. If your shared code is stable and your active development is in isolated apps, the coordination savings are small.
You have a single deployable app with no shared packages. There's no coordination cost to eliminate. Stay in a polyrepo.
These aren't a score. They're a way to make the real tradeoffs visible before you're already committed to a structure.
Copy link to headingThe build graph is the actual product
Teams that run monorepos well think about the build graph: what each package depends on, what changed since the last run, and whether any given task actually needs to execute. The directory structure is incidental. The graph is what you're operating.
That focus on the build graph changes where you spend time. A team focused on directory layout invests in conventions, ownership rules, and import policies. Useful work, but none of it affects build speed. A team focused on the build graph invests in task definitions, cache input configuration, and affected-package detection. That work determines whether CI stays fast as the codebase grows.
We made turbo run execution up to 96% faster, working with AI agents inside Vercel Sandboxes. And we made Remote Cache free across all plans because a well-configured cache turns a monorepo from a coordination tool into a genuine productivity multiplier. The repo structure gets your code into one place. The build system determines what your team can actually ship.
Copy link to headingFAQ
Copy link to headingDo I need Turborepo to run a monorepo on Vercel?
No. Vercel deploys monorepos by linking multiple projects to a single repository, each with its own Root Directory, and automatically skips unaffected projects. Turborepo adds incremental builds through task orchestration and Remote Caching. Remote Caching is automatically enabled on Vercel when you run turbo commands during a Vercel Build. The Ignored Build Step also supports git diff path filtering and npx turbo-ignore for Turborepo-based repos, as well as npx nx-ignore for Nx-based repos.
Copy link to headingIs Vercel Remote Cache free?
Yes. Vercel Remote Cache is free on all plans, including for teams not hosting on Vercel. Upload limits differ by plan: 100 GB/month on Hobby, 1 TB/month on Pro, and 4 TB/month on Enterprise.
Copy link to headingWhy does my Next.js build always miss the Turborepo cache?
Yes, this is expected when Skew Protection is enabled. Skew Protection injects a deployment ID as an environment variable at build time. That variable is part of the task hash, so the hash changes on every deploy and the cache always misses on the build step. It's a deliberate tradeoff between version safety and build caching. Keep both enabled and plan for the build step to run fresh each time.
Copy link to headingCan Turborepo handle a polyglot monorepo with Go or Python?
Turborepo covers JavaScript and TypeScript. Non-JS packages need separate tooling, which reintroduces some of the coordination overhead a monorepo is meant to remove. Polyglot teams should plan explicitly for what Turborepo won't handle before committing to a single repo.