One Package Manager's Dependency Resolution Cost a Team Its Entire Monorepo
A mid-size SaaS company with roughly 40 engineers decided to consolidate a handful of frontend and backend services into a single repository. The initial pitch was straightforward: share types, reuse utility libraries, and run a single CI pipeline. For the first two years, it worked. Then the package count crossed 400, then 600, and eventually settled near 800 packages. That was when the dependency resolution time began to feel like a tax on every commit.
A typical npm install took 45 to 55 minutes on the CI runner. Developers working on isolated features would push a branch, trigger the pipeline, and then wait nearly an hour to learn whether their change passed the linting stage. The team tried incremental builds, caching node_modules across runs, and even throwing faster hardware at the problem. Nothing brought the install time below 40 minutes. The monorepo that was supposed to accelerate development had become a bottleneck of its own.
The Monorepo That Took 50 Minutes to Resolve Dependencies
The team in question ran a mid-size SaaS platform serving a few hundred enterprise customers. Their monorepo held around 800 packages: shared UI components, API clients, internal tooling, and microservices. Each package declared its own dependencies, and the dependency graph had grown organically over three years. No one deliberately designed it to be complex; it just accreted.
The breaking point came during a routine sprint. A junior engineer added a small utility package that happened to introduce a version conflict with a deeply nested dependency used by twelve other packages. The CI pipeline ran for 40 minutes before npm install failed with a cryptic peer dependency error. The team spent the rest of the day debugging the resolution tree. By the end of the week, the engineering director had called a meeting to discuss splitting the monorepo.
This story is not unique. While exact percentages are hard to pin down—companies rarely publish these metrics—anecdotal evidence from engineering blogs and conference talks suggests that many teams using monorepos with more than 500 packages experience resolution times that degrade developer productivity by a measurable margin. The tooling that works for a 50-package project breaks down at scale.
The frustration boiled over when a senior developer timed a full CI run during a sprint retrospective. The install phase alone consumed 55 minutes of a 90-minute pipeline. The rest of the build—compilation, tests, linting—took 35 minutes. The team calculated that each developer spent roughly four hours per week waiting on dependency resolution. Over a quarter, that amounted to a person-month of lost productivity. Something had to give.
How One Package Manager's Resolution Algorithm Became the Bottleneck
To understand why resolution times balloon, it helps to look at how package managers handle dependency graphs. npm v7 introduced a flat-tree strategy that aims to deduplicate packages by hoisting shared dependencies to the top of node_modules. This reduces disk usage but increases the time spent computing the optimal layout. For a graph with 800 packages and tens of thousands of transitive dependencies, the algorithm must evaluate many candidate layouts before settling on one. The algorithm's complexity is roughly O(n^2) in the number of packages, though optimizations like topological sorting and caching can reduce that. Still, as the graph grows, the resolution time grows superlinearly. Some estimates put the resolution phase at 60–80% of total install time for repos with more than 500 packages.
Yarn Berry (v2+) took a different approach with Plug'n'Play (PnP). Instead of a node_modules folder, it uses a single lockfile that maps package names to zip archives. PnP eliminates the disk I/O of reading thousands of folders, but it introduces overhead during resolution because the algorithm must resolve peer dependencies and virtual packages more aggressively. In practice, teams that adopted Yarn Berry for large monorepos reported resolution times that were sometimes faster, sometimes slower—but rarely below 30 seconds for repos over 500 packages.
The lockfile itself becomes a problem. A lockfile for an 800-package monorepo can easily exceed 15,000 lines. Every time a dependency is added or updated, the lockfile must be recomputed. The fundamental issue is that package managers are designed for satisficing—finding a good enough resolution—rather than optimal resolution. npm's algorithm, for example, uses a greedy heuristic that works well for small graphs but can get stuck in backtracking loops when peer dependencies conflict. Yarn Berry's PnP algorithm uses a more exhaustive search, which is more reliable but slower. Neither tool was designed with a thousand-package monorepo in mind.
The Breaking Point: A Single Dependency Chain Triggered the Collapse
At the company in question, the trigger was a seemingly innocuous utility package that had been published by a former intern. It wrapped a left-pad-like string manipulation function and had been used in a single frontend project. Over time, other packages began depending on it indirectly. When a new version of a major framework required a different version of that utility, the conflict rippled through twelve packages that all needed different versions of the same underlying library.
npm's resolver tried to satisfy all constraints by creating multiple copies of the conflicting package—one for each version range. But because the packages were deeply nested, the hoisting algorithm could not find a flat layout that satisfied all peer dependencies. After 40 minutes of computation, npm threw an error: ERESOLVE unable to resolve dependency tree. The error message gave no clue about which combination of packages caused the conflict. The team had to manually inspect the lockfile and trace the dependency graph using tools like npm ls.
That single debugging session consumed a full day of a senior developer's time. The root cause turned out to be a version constraint in a package that had not been updated in two years. The package's maintainer had specified a peer dependency on the utility library with a caret range that clashed with the framework's requirement. Once identified, the fix was a one-line change to the version range. But the cost of finding that line was enormous.
The team decided to split the monorepo into six smaller repositories, each with its own package.json and CI pipeline. The decision was not made lightly. Splitting a monorepo means duplicating shared infrastructure, managing cross-repo versioning, and losing the ability to refactor across boundaries atomically. But the alternative—continuing to lose a person-month per quarter to resolution time—was untenable.
Splitting a Monorepo Is Not a Technical Decision—It's a Social One
The split at the company took roughly two to three months to stabilize, a timeline that aligns with industry anecdotes for similar efforts. The first step was identifying ownership boundaries. The team had a bus factor of about three people—meaning if those three left, critical knowledge about package boundaries would be lost. The split forced the team to document which packages belonged to which service, which dependencies were shared, and which were incidental.
Once the boundaries were drawn, the CI cost initially doubled. Each of the six repos had its own pipeline, and shared packages had to be published to an internal registry and versioned independently. The team had to set up cross-repo CI triggers to rebuild dependent services when a shared package changed. The total compute time increased, but the per-developer wait time decreased because each repo had far fewer packages. A typical install in the new repos took two to three minutes.
Developer autonomy increased significantly. Teams could now update dependencies in their own repo without coordinating a monorepo-wide lockfile change. The frontend team could upgrade React without waiting for the backend team to approve the change. The split also made the bus factor visible: each repo had a designated owner, and the dependency graph between repos was explicitly versioned. The social contract shifted from "we all share everything" to "here is our public API."
But the split was not without costs. Shared UI components that had been easy to refactor across the monorepo now required a publish-and-update cycle. The team had to adopt a tool like Changesets to manage version bumps. Some engineers missed the atomic refactoring ability. The trade-off was clear: faster day-to-day development at the expense of cross-cutting changes. For the team, the trade-off was worth it. For other teams, it might not be.
Package Managers Are Still Optimized for Small Repos
The story of this team is not an indictment of any single package manager. npm, Yarn, and pnpm all have strengths, but none of them were designed for the scale of an 800-package monorepo. pnpm's content-addressable store, which stores a single copy of each package version on disk regardless of where it is used, reduces disk usage and install time by roughly 30–50% compared to npm for large repos. But the resolution phase still dominates.
Bun, a newer runtime that includes its own package manager, claims faster installs by using a custom binary format for the lockfile and a highly parallelized resolver. As of early 2025, Bun's package manager is still in beta, and some early benchmarks suggest it can resolve dependencies 10–20x faster than npm for repos under 200 packages. But for repos over 500 packages, the performance advantage narrows, and some users report edge cases where Bun's resolver produces different dependency trees than npm, leading to subtle runtime bugs.
Benchmarks from GitHub Actions and self-hosted CI runners consistently show a superlinear slowdown past 500 packages, regardless of the package manager. The exact inflection point varies by tree shape—a deep, narrow tree resolves faster than a wide, shallow one—but the trend is clear. No mainstream package manager handles 1,000+ packages gracefully. This is a gap in the ecosystem that tooling vendors have been slow to address.
The maintainer perspective is often overlooked. Package manager maintainers are a small group—the npm CLI, for example, has historically been maintained by a handful of engineers at npm, Inc., and later by GitHub. Their priorities are stability, security, and backward compatibility, not performance at extreme scale. The funding for large-repo optimization comes from companies that hit the wall, but those companies often build internal solutions rather than contributing upstream. The result is a slow feedback loop.
Lessons for Teams Hitting the Same Wall
What can teams do before they reach the breaking point? The first step is to measure resolution time per package. Adding a simple timing wrapper around npm install and logging the duration to a dashboard can reveal trends before they become crises. If the install time exceeds 10 minutes for a repo with fewer than 300 packages, it is worth investigating the dependency tree for cycles or excessive duplication.
Consider virtual workspaces early. Tools like npm workspaces and Yarn workspaces allow a monorepo to share a single node_modules at the root, which reduces duplication but can still suffer from resolution overhead. For repos with more than 500 packages, pnpm's workspace protocol is often a better choice because it leverages the content-addressable store and avoids hoisting conflicts. The trade-off is that pnpm's strict module isolation can break packages that expect hoisted dependencies.
Cache lockfile artifacts aggressively. Most CI systems allow caching of node_modules or the package manager's store. If the lockfile has not changed, the install step can be skipped entirely. This is obvious advice, but many teams skip it because they assume the cache will be invalidated frequently. In practice, a well-configured cache can reduce install time to under a minute for unchanged lockfiles.
Audit dependency trees quarterly. Use tools like npm audit, yarn audit, or dedicated dependency analysis tools to identify deprecated packages, version conflicts, and unnecessary transitive dependencies. A quarterly audit can catch problems before they cascade. Some teams set a policy that any package with more than 50 transitive dependencies must be reviewed before being added. The overhead is small compared to the cost of a resolution failure.
The Monorepo Dream Hinges on Tooling, Not Scale
Google's internal monorepo, which contains billions of lines of code, famously uses Bazel to manage dependencies. Bazel's hermetic build system avoids the npm-style resolution entirely by computing a deterministic build graph from explicit dependencies. But Bazel is not portable to most organizations. Its learning curve is steep, and its language-agnostic approach requires significant investment in build rules. For most teams, Bazel is overkill.
Tools like Nx and Turborepo help with task orchestration and caching, but they do not fix the underlying dependency resolution problem. They can skip a build if the inputs have not changed, but they cannot make npm install faster. The package manager remains the bottleneck. Some teams have experimented with importing a pre-resolved lockfile into the CI image, avoiding resolution entirely. That approach works until a dependency changes.
The maintainer perspective is again critical. Open-source package managers are funded by a mix of corporate sponsorships, donations, and (in npm's case) a commercial registry. The incentive to optimize for 800-package monorepos is low because the number of teams at that scale is small. A robust solution will likely require 5–7 years of incremental improvements, or a new entrant that prioritizes large-repo performance from the start.
Splitting a monorepo is one option, but it is not the only path. Some teams invest in faster package managers like pnpm or Bun, adopting them early to delay the pain. Others adopt Bazel-like tools, accepting the upfront investment for long-term scalability. Still others implement strict governance on dependency additions, capping the number of packages or enforcing regular audits. The dream of a single repository that scales without friction is real for small teams, but for those pushing past 500 packages, the dream comes with a tax. The tax is paid in minutes, then hours, then days. The choice is not between monorepo and multi-repo; it is between paying the tax and investing in alternatives. For some teams, splitting is the right call. For others, a combination of tooling upgrades, caching, and governance can extend the monorepo's life. The key is to measure, plan, and choose deliberately.