Monorepos used to feel like an opinion. In 2026, for a team of more than about three engineers sharing any real code, they feel like the default. The tooling got good enough that the “too much overhead” argument is mostly gone. So we picked a realistic project shape (a Next.js web app, a Hono API, and a shared TypeScript library) and set it up three ways: Turborepo 3 with pnpm 10, pure pnpm workspaces, and pure Bun 1.4 workspaces. Same code, three configs, same laptops, same CI.
The short version: they are all usable. The interesting differences show up in cold-install time on CI, how painful it is to add the tenth package, and how long it takes for a new hire to feel productive. Here is what we measured and where each one broke for us.
The test project
The apps are realistic but small enough to reproduce. The web app is a Next.js 16 App Router project. The API is a Hono service deployed to Cloudflare Workers. The shared lib (@acme/core) holds Zod schemas, a few utility functions, and the shared TypeScript types. Total lines of code across the three: about 4,200. We added a @acme/ui package later for the medium-team test, which pushed us to five packages.
All measurements are from a MacBook Pro M5 Pro (24GB RAM) on a warm home connection and from GitHub Actions ubuntu-latest using the default 4-vCPU runner. Every number is the median of five runs. We ran them all in one sitting to keep network noise roughly equal.
Path 1: Turborepo 3 + pnpm 10
This is the setup most large-team monorepos end up at. pnpm handles the install and hoisting, Turborepo handles the task graph and caching. The config is declarative and the remote cache is the killer feature nobody talks about until they have it.
Our package.json at the root:
{
"name": "acme",
"private": true,
"packageManager": "pnpm@10.7.0",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"lint": "turbo run lint",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "^3.1.0",
"typescript": "^5.7.0"
}
}And the turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**", "!.next/cache/**"]
},
"dev": { "cache": false, "persistent": true },
"lint": {},
"test": { "dependsOn": ["^build"], "outputs": ["coverage/**"] }
}
}Cold install on our laptop: 21 seconds. On CI (fresh, no cache): 38 seconds. Incremental build of the web app, no changes, cache warm: 420 milliseconds. Full rebuild from cold cache: 48 seconds across all three packages in parallel.
The win is the remote cache. We turned on Turborepo’s hosted remote cache and PRs that only touch the API went from 2m 10s to about 45s of CI time, because the web build is served from cache without rebuilding. At a team of eight pushing 40+ PRs a week, that paid for itself in week two on saved runner minutes alone.
Path 2: pure pnpm workspaces
No Turborepo. Just pnpm -r and workspace protocol. The pitch is that pnpm has gotten good enough that you may not need a second tool. For small teams this is genuinely true.
// package.json (root)
{
"name": "acme",
"private": true,
"packageManager": "pnpm@10.7.0",
"scripts": {
"build": "pnpm -r --filter \"./apps/*\" build",
"dev": "pnpm -r --parallel --filter \"./apps/*\" dev",
"test": "pnpm -r test"
}
}
// pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"Cold install: 19 seconds on laptop, 34 seconds on CI. Install time beat Turborepo’s path by a couple of seconds because there is one fewer dev dependency. Incremental build was the same (pnpm is not doing the caching, your bundler is). Full build from scratch: 49 seconds, essentially identical.
Where this falls over is when you have more than one CI job. Without Turborepo you either rebuild everything on every job or you hand-roll a change detection script. We tried both. The hand-rolled script (a 15-line bash git diff filter) worked fine until someone added a path-based workspace dependency and we spent a whole morning debugging why a shared lib change was not triggering a web rebuild.
pnpm workspaces alone are great until you have more than two apps. The moment you have three, you are reinventing Turborepo, poorly.
Path 3: pure Bun 1.4 workspaces
Bun in 2026 is a real contender, not a toy. The install is the fastest thing we have ever seen on a Node-compatible stack, and the built-in test runner is genuinely good. For a greenfield monorepo where everything targets runtimes that are Bun-compatible, it is compelling.
// package.json (root)
{
"name": "acme",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "bun run --filter '*' build",
"dev": "bun run --filter 'apps/*' --parallel dev",
"test": "bun test"
}
}Cold install: 3.1 seconds on laptop, 6 seconds on CI. Not a typo.bun install absolutely smokes pnpm on cold installs, and the gap widens with more packages. On our laptop the install is essentially instant.
The catch is real. Next.js works on Bun as a runtime for most things, but we hit one native-module edge case around Sharp image processing that forced a node fallback for the build step. Cloudflare Workers deploy is agnostic, so Hono was fine. Your mileage will vary depending on how exotic your dependencies are. Check the native-module situation for anything that does image, crypto, or database driver work.
Bun without Turborepo vs Bun with Turborepo
Turborepo runs fine on top of Bun. You get Bun’s install speed plus Turborepo’s caching. We tested this hybrid too. Cold install stayed around 3.5 seconds. Remote-cached builds behaved the same as the pnpm+Turborepo path. The main downside is running two toolchains that both want to own the task graph. If you are going to use Turborepo, the ecosystem signal is stronger around pnpm, and the marginal install time you save with Bun matters less once caching kicks in.
CI costs, the part nobody budgets for
We tracked CI minutes over two weeks with each setup, using the same PR cadence (roughly 40 PRs per week, plus a nightly full-build job). GitHub Actions standard runners at $0.008 per minute. Numbers are per week:
- Turborepo + pnpm, remote cache on: 340 runner minutes = $2.72
- Pure pnpm, no caching: 1,220 runner minutes = $9.76
- Pure Bun, no Turborepo caching: 980 runner minutes = $7.84
The gap between Turborepo with remote cache and everything else is almost entirely about cache hits on PRs that only touch one package. At 40 PRs a week, the pnpm-only setup spent 28 extra minutes per day rebuilding things that did not need rebuilding. Over a year, that is $360 of runner minutes for a team our size. The number gets worse linearly with team size.
If you are using a cloud provider’s own CI (CodeBuild, Cloud Build, a self-hosted runner), the math shifts because you are paying for committed capacity instead of per-minute. That is where Turborepo’s value also lowers: if your runner is already paid for and idle, you care less about shaving minutes off.
Developer onboarding, measured in days
We shipped onboarding docs for all three setups and timed two new engineers through each of them on different weeks. The task: clone the repo, get the dev server running, make a small cross-package change, see it rebuild, open a PR. Time from git cloneto “merged PR”:
- Turborepo + pnpm: 2.5 hours median
- Pure pnpm: 3 hours median (one engineer hit a workspace protocol bug)
- Pure Bun: 4 hours median (one engineer hit the native module issue)
Turborepo + pnpm wins the onboarding race mostly because its docs are better and because StackOverflow has a decade of answers when things break. Bun is new enough that when something weird happens you are reading GitHub issues, not articles.
The best monorepo stack is the one the most junior engineer on your team can debug at 3am without paging anyone. In 2026 that is still Turborepo plus pnpm.
Who should pick what
We came out of the week with real opinions, not a “it depends” shrug.
- Solo / 1-3 engineers, 2-3 packages: pnpm workspaces alone. Or Bun if your stack tolerates it. Adding Turborepo is extra cognitive load you do not need.
- Small team / 4-10 engineers, 3-8 packages: Turborepo + pnpm. The remote cache starts paying for itself as soon as you have more than a couple of people pushing PRs an hour.
- Medium team / 10-50 engineers, 8+ packages: Turborepo + pnpm, with a serious investment in the remote cache and CI pipeline. Consider adding Nx on top if you want task orchestration that understands your TypeScript dependency graph at a deeper level (we did not for our test, but several friends have).
- Greenfield, 2026-native stack: Bun workspaces alone. If you are writing Hono APIs for Workers and Next.js for Vercel and nothing exotic, the simplicity is worth it.
The stuff that bit us
A few things we hit that the tutorials do not mention.
Workspace protocol. All three setups use workspace:* to reference internal packages. Do not commit a built-in version number here, and do not check in a compiled dist/ for your internal packages. Let your build tool chain handle it. A teammate checked in dist/“to speed up CI” and we spent a week debugging why a type change was not propagating.
TypeScript project references. You do not need them. In 2026, with modern bundlers and tsc --build improvements, plain path-mapped imports via tsconfig.json and each package having its own tsconfig.json that extends a shared base, work well enough. We started with project references on the first run and ripped them out on the second. They save maybe 10% on cold builds and add significant config complexity.
ESLint and Prettier configs. Put the shared config in a package (@acme/config) and have each workspace depend on it. Seems obvious, but we have seen teams copy-paste .eslintrc into every package and then wonder why their rules drift.
What we’d actually do
For a new project tomorrow at our team size (roughly eight engineers), we would pick Turborepo 3 + pnpm 10. The install is fast enough, the config is readable, the remote cache changes the math on CI costs, and the ecosystem is the one your new hires will have seen. Bun workspaces is a genuine second choice for a greenfield team that has reviewed its dependency list and knows nothing breaks.
The thing nobody tells you: the tooling matters less than the conventions you write down. A team with a mediocre monorepo setup and a one-page README explaining how to add a package will out-ship a team with the world’s best Turborepo config and no shared understanding of it.