A consistent(-ish) npm dependency graph
registry

A consistent(-ish) npm dependency graph

22% of packages arrive on the npm registry without their dependencies available yet.

A piece of art showing a graph invoking graph-like feelings

In recent years it's gotten more popular for large open source projects to publish many interdependent packages and bulk-ship changes. Often they live in a monorepo, and they might have some CI task that checks all the packages for changes since the last release, and bulk-releases the ones that have changed. Sometimes they don't even bother and just bulk-release everything at once without checking whether it's needed.

But what happens if you need to ship a package and that package needs a change in one of its dependencies?

For example, let's take @aws-sdk/client-s3. Perhaps a new feature got added, but it requires a change from @aws-sdk/core to be shipped as well.

The obvious way to handle this is to publish @aws-sdk/core first, wait until it's available on npm (after scanning, CDN delays, staged publishing) and then ship @aws-sdk/client-s3, bumping the dependency version of core.

In practice, most people don't do this. In the AWS case they currently have 1,057 packages in the @aws-sdk/ namespace, and the graph can get deep. Most projects tend to just publish everything in parallel without thinking about dependency relationships.

What this means in practice is that these packages and their dependents can arrive on npm in any order, and at the time of publishing might have dependencies that don't exist on npm (yet). npm does not validate your dependencies at publish time, so you can even refer to packages that will never exist.

npm has always been 'eventually consistent', but more people have started noticing this recently after npm added scanning, creating longer, less predictable windows in which new versions are not available yet. It doesn't help that in the staged publishing UI you have to click packages one at a time, and this UI is blocked until the scanning step completes.

For a real example, see this bug report for Effect-TS in which people are prevented from installing due to the dependency graph being only partially available, or this report from the Cloudflare team, which discusses issues with Wrangler releases.

This means that if you are a maintainer of a project like Effect, your users might literally not be able to install your package until npm has 'settled' and all packages are available again.

As I'm writing this article, in the last 24 hours 175,891 new versions were released, and 22% of those (38,995) landed on the registry with a dependency that could not be resolved.

With this in mind, we've decided to make some changes to our npm replication system so that we temporarily hold onto package publishes if transitive dependencies have not yet been published as well. These changes mean end users get far more consistent and reliable dependency graphs when using our registry over registy.npmjs.com.

The system is not perfect, but we're able to smooth out these releases for about 96% of cases. We wait at most an hour, so the remaining packages stay on hold for an hour and then we release them anyway. Our system even supports circular dependencies, which are also a thing on npm!

For 96% of publish events we are able to facilitate a coordinated, smooth release process of the interdependent packages within a few minutes. The remaining 4% of publishes can wait up to an hour before we time out and release them even though their full set of dependencies is not yet available. Notably, this system accounts for circular dependencies, which happen often in the ecosystem, while ignoring mutable, non-registry dependencies (like git repositories).

When digging into the remainder, we found that these were usually packages that have dependencies (or peer or optional dependencies) that don't exist at all. We suspect that a bunch of these are packages from companies that refer to internal private packages and thus probably don't have much real external usage, and in some cases it can just take a long time for package maintainers to publish because of the current state of the npm UI.

We'll continue to monitor the ecosystem's publishing and consumption habits and may adjust our strategy and timeouts accordingly in the future. That said, if you've ever found yourself in one of these edge cases or know of other nuances we haven't yet addressed, we'd love to hear from you!