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.
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.
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:
#!/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:
- Tier 1 (Critical path): Packages used at runtime in production code paths. A failure here means an outage.
- Tier 2 (Build-time): Packages used only during build, test, or CI. A failure here means blocked deploys, not outages.
- Tier 3 (Optional/dev): Linting plugins, formatting tools, editor integrations. Annoying to lose, not dangerous.
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.
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.
# 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