It was a Tuesday. The kind of quiet Tuesday where nothing is supposed to break. Staging had been green for six days. The team was heads-down on a feature branch, nowhere near the deployment pipeline. And then, at 10:47 AM, every container on our staging cluster began restart-looping.

The error was inscrutable at first: a MODULE_NOT_FOUND for a package none of us had heard of. It lived four levels deep in our dependency tree — a transitive dependency of a transitive dependency of a library we used for date formatting. The maintainer had unpublished the package twelve hours earlier, quietly, with no deprecation notice and no forwarding.

This is a story about that morning. But it is also a story about the assumptions we make when we type npm install, about the invisible architecture that holds modern applications together, and about what happens when one small brick is removed from a wall you didn't know existed.

The Archaeology of a Lockfile

The first thing I did, once we had confirmed this wasn't a network issue or a registry outage, was open the package-lock.json. I had seen this file thousands of times. I had committed it, merged it, resolved conflicts in it. But I had never truly read it.

Our application had 1,847 packages in its dependency tree. We directly depended on 43 of them. That means roughly 97.7% of the code we shipped was chosen by someone else — or, more accurately, by some other someone else's choices, recursively.

A dense tree visualization representing npm dependency chains
Fig. 1 — A partial visualization of a typical Node.js project's dependency tree. The highlighted node is four levels deep, depended upon by 23 other packages.

I started tracing the path. Our application imported @company/date-utils, a thin wrapper our team had written around dayjs. dayjs itself has an excellent plugin ecosystem, and one of those plugins depended on a small utility called locale-data-compact. That utility, in turn, pulled in unicode-cldr-segments. And that package, maintained by a single developer in their spare time, was the one that vanished.

Aside

I want to be clear: I don't blame the maintainer. Open-source maintainers owe us nothing. The failure here was systemic — an ecosystem that makes it trivially easy to depend on code maintained by one person with no contractual obligation to keep it available.

What a Dependency Graph Actually Is

We talk about "dependencies" as if they are lines on a chart. In practice, they are more like load-bearing walls in a building you inherited. You can see the walls on a blueprint, if you have one. But you cannot tell which walls are decorative and which are structural by looking at the blueprint alone. You find out when you remove one.

Every dependency is a bet that someone else will continue to care about the same problem you care about, in the same way, for as long as you need them to.

— Russ Cox, "Our Software Dependency Problem"

The npm ecosystem is, by design, deeply nested. The Node.js philosophy encourages small, focused modules. This is elegant in theory. In practice, it means your application's stability depends on the continued goodwill and attention of hundreds of strangers.

The Fix (and the Real Fix)

The immediate fix was straightforward. We pinned the missing package to a version we had cached in our CI artifacts, added it to a local registry mirror, and deployed. Total downtime: 47 minutes. Not catastrophic, but embarrassing for staging — and a clear warning about production.

The real fix took longer. We spent the next two weeks doing a dependency audit that I now think every team should do annually. Here is the process we developed:

Step 1: Map Your Transitive Dependencies

Use npm ls --all to generate a full tree. Then extract unique package names and check their maintenance status:

audit-deps.sh bash
#!/bin/bash
# Generate a list of all unique transitive dependencies
# and check their last-publish date

npm ls --all --json 2>/dev/null \
  | jq -r '.. | .dependencies? // empty | keys[]' \
  | sort -u \
  | while read pkg; do
      last_publish=$(npm view "$pkg" time.modified 2>/dev/null)
      maintainers=$(npm view "$pkg" maintainers 2>/dev/null | wc -l)
      printf "%-40s  %s  maintainers: %d\n" "$pkg" "$last_publish" "$maintainers"
    done | tee dep-audit-$(date +%Y%m%d).txt

This gave us a sobering picture. Of our 1,847 dependencies, 312 had a single maintainer. 89 hadn't been published in over two years. 14 had GitHub repositories that no longer existed.

Step 2: Classify Risk Tiers

Not every dependency carries equal risk. We classified ours into three tiers:

We then cross-referenced tier with maintenance status. Any Tier 1 dependency with a single maintainer and no activity in 12+ months went on a watchlist.

A developer reviewing dependency audit results in a terminal
Fig. 2 — The output of our audit script, color-coded by risk tier. Red entries are single-maintainer packages in the critical path with no recent activity.

Step 3: Establish a Local Mirror

We set up Verdaccio as a local npm registry proxy. Every package we install is now cached locally. If the upstream disappears, we still have a copy. This is not a complete solution — it doesn't protect against malicious updates — but it handles the "vanishing maintainer" scenario cleanly.

.npmrc ini
# Point npm to our Verdaccio proxy
registry=http://registry.internal.company:4873/

# Verdaccio proxies to npmjs.org and caches locally
# See: verdaccio/config.yaml for uplink configuration

What I Think About Now

This incident changed how I think about dependencies. Not dramatically — I still use npm, I still install packages, I still trust the ecosystem more often than not. But I now think of package.json less like a shopping list and more like a load-bearing contract.

Every line in that file is a relationship. Some relationships are with large, well-funded organizations that have strong incentives to maintain stability. Others are with individuals who might be going through a difficult year, or who simply lost interest in a problem they solved five years ago.

Neither type of relationship is wrong. But pretending they are interchangeable — that a dependency is a dependency is a dependency — is a form of technical naivety that costs real time and real money when it fails.

The packages you install are not your code. But they become your responsibility the moment a user encounters a bug in them.

I've started telling junior developers on my team: before you install a new package, answer three questions. Who maintains it? What does it depend on? What happens if it disappears tomorrow? If you can't answer all three, you are not ready to add that dependency.1

This might sound overly cautious. It probably is. But I would rather be the person who asks one unnecessary question than the person who spends a Tuesday morning explaining to a stakeholder why staging has been down for an hour because of a package they have never heard of, maintained by someone none of us have ever met, four layers deep in a tree none of us have ever read.

The dependency you didn't know you had is the one that will teach you the most.2

Notes
1. This is not original advice. Russ Cox's 2019 essay on software dependencies articulates this more rigorously than I can. The Go module system was designed with these concerns in mind. The Node.js ecosystem has not yet caught up.
2. After this incident, I wrote a small CLI tool that generates a "dependency health report" for any Node.js project. It's available on GitHub at github.com/FredLackey if you find it useful. MIT licensed, single maintainer. The irony is not lost on me.