I run a one-person software studio. Last week I built a dated index of "stale but critical" npm packages. This post is the walkthrough: the method, the numbers, what surprised me, and the caveats that keep the headline honest.
npm packages don't expire. A package published once in 2015 with a loud deprecation notice keeps flowing into npm install for as long as other packages depend on it. Weekly download counts tell you a package is alive in the ecosystem; they say nothing about whether anyone has shipped anything for it lately. So I asked the boring version of the supply-chain question: of the packages the JavaScript world downloads the most, how many haven't published a release in a year or more? (Snapshot as of 2026-10-09/10; some of these are simply finished — the question is which.)
The answer: 2,905 packages carry at least 1,000,000 weekly downloads. All 2,905 — not a sample of them — were checked individually against the registry. 887 — about 30% — had no release in the last 12 months. 630 of those haven't released in 24+ months.
First, a method story: the npm registry deleted its own index
I expected this to start with downloading npm's full package index. That's no longer possible: the registry retired its enumeration endpoints — /-/all, /-/v1/all, /_all_docs all return 404 now (I verified each). The 4GB JSON index that every "list all npm packages" tutorial tells you to download is gone. There is currently no complete, public way to enumerate the registry.
So the universe had to be reconstructed. I seeded it two ways: every GitHub repository in the top 1,000 most-starred JavaScript and TypeScript repos that has a root package.json (its dependencies and devDependencies are real npm names), plus a series of popularity-based search-API sweeps over npm itself. That produced 24,524 candidate names, of which ~23,900 had weekly-download data.
From there, two rules kept the numbers honest:
- Only primary data. Weekly downloads come from npm's downloads API; publish history comes from each package's packument (the registry's own record). I fetched all 2,905 packuments for the ≥1M/week set individually — no sampling, no estimates.
- "Stale" means "no release," not "no commits." A package whose repo is actively pushed but hasn't published is a different beast from a package nobody has touched since 2019. I'll come back to that distinction — it turned out to be the most interesting part.
The top of the list
The 30 highest-download flagged packages, sorted by weekly downloads (dates are last publish; repo data where the packument pointed at GitHub):
| package | dl/week | last release | repo last push | open issues |
|---|---|---|---|---|
| debug | 576.9M | 2025-09-13 | 2026-04-01 | 100 |
| json-schema-traverse | 340.8M | 2020-12-13 | 2021-07-26 | 14 |
| find-up | 328.0M | 2025-09-16 | 2026-09-18 | 1 |
| safe-buffer | 317.3M | 2020-05-10 | 2023-06-03 | 16 |
| mime-db | 295.0M | 2025-03-18 | 2026-09-08 | 55 |
| @jridgewell/trace-mapping | 265.6M | 2025-09-10 | 2026-08-28 | 8 |
| json5 | 239.8M | 2022-12-31 | 2024-10-25 | 40 |
| convert-source-map | 224.1M | 2022-10-17 | 2025-03-27 | 7 |
| strip-json-comments | 216.0M | 2025-08-08 | 2026-09-18 | 0 |
| node-fetch | 206.5M | 2023-08-23 | 2026-05-12 | 249 |
I spot-checked the top 10 against the GitHub API directly (repo archive status, last push, open issues). None of the ten is archived. Six have been pushed within the last six months. debug — quite possibly the single most-installed package that isn't part of a framework — published nothing since September 2025 while its repo was pushed in April 2026. find-up and strip-json-comments were pushed within days of my check.
And then there's node-fetch: 206 million weekly downloads, a last release from August 2023, and 249 open issues on a repo that was still being pushed in May 2026. That's not an abandoned package. That's a release process that stopped happening.
"Stale" is two very different things
The spot-check suggested the list has two distinct populations, so I did the full split. A note on units first: the 887 is a count of packages; resolving each package's packument repo URL to GitHub gives 829 repos (50 packages had no repo URL, 8 lookups failed — several packages can share one repo). For each resolved repo I pulled the last-push date and archive status.
| repo last push | repos | share of 829 |
|---|---|---|
| ≤6 months — "release-lag" cohort | 302 | 36% |
| — of which ≤30 days | 201 | 24% |
| 6–12 months | 77 | 9% |
| 1–2 years | 123 | 15% |
| 2+ years — dormant | 327 | 39% |
| memo: formally archived (already counted inside the rows above) | 52 | 6% |
A third of the stale-but-critical list is actively developed and simply not released — and 201 of those 302 repos were pushed within the last month. These are the "maintainer is alive, the release step isn't" cases: monorepo tooling drift, release-checklist rot, "I'll cut a version when the feature lands." From a risk point of view they're the opposite of scary in one way (a human with write access is demonstrably present) and scary in another (nobody has validated a shipped artifact against today's dependencies in a year).
The dormant core is different: 327 packages with no push in 2+ years, plus 52 formally archived. That's where the classic orphan-package risk actually lives — unmaintained code that still shows up in lockfiles because something three levels deep depends on it.
The caveats, stated plainly
Three things make the headline softer than it looks, and you should know all of them before quoting the number.
Monorepo mapping inflates the release-lag cohort. Repo URLs come from packuments, and monorepo sub-packages inherit the parent repo's push date. The three @material-ui/* packages in my flagged set all map to mui/material-ui (99k stars, pushed today — because the monorepo is pushed today, not because those sub-packages moved). Same for create-react-class and friends pointing at the React repo. So the 302 is an upper bound on the true "actively developed, no release" count — some of those sub-packages are likely dormant and merely inherit their parent's activity — and the dormant share is a corresponding lower bound. The direction of the bias is known; fixing it (per-subdirectory activity) is the v2 work.
The universe is seeded, not exhaustive. With /-/all retired there is no complete registry enumeration to draw from. My seed (top-starred GitHub repos + popularity sweeps) under-covers packages without a GitHub footprint. A 1M+/week package is very likely to be on GitHub, which keeps that under-coverage small — but it isn't zero, and I'd rather say so than pretend the seed was random.
"No release" is a registry-side proxy. Publish history can miss git-tag-only distribution and treats yank/republish edge cases bluntly; it's the right signal for most packages, but it's a proxy, not a proof of inactivity.
Downloads are not criticality. A single large consumer (a bundled CLI, a framework) can put a package on this list that most projects never touch directly. Conversely, genuinely critical packages below 1M/week aren't counted at all. This index answers "which widely-downloaded packages have stopped releasing," not "which packages are the most important to secure."
Is the number stable?
I re-ran the whole pipeline the next day: 886 → 887 (one new flag, react-inspector), zero packages dropped, 629 → 630 in the 24-month band. Day-over-day, the list is essentially static — which is itself the point of making it dated: the value is in being able to diff it month over month and watch which cohort grows.
What I'd actually do with this
Not fear. Most of these packages are fine: debug and mime-db are stable by design; "finished" is a legitimate state for a utility library. But two habits fall out of the data:
- When you see a 12+ month-old dependency in a lockfile, check the repo, not just the publish date. If the repo was pushed last month but nothing shipped since 2023, you've learned something specific: the maintainer exists, and the release pipeline is the bottleneck. If the repo is archived, you know that too.
- If you maintain something on this list and it's in the release-lag cohort: your users can see the 12-month-old publish date but not the recent commits. A tagged, validated publish closes that gap — and if the package is simply finished, a README line saying so is just as valuable.
The full machine-readable list — all 887 rows with downloads, last-publish date, repo URL, and where known the push date and archive status — is published as a dated data file here: npm critical+stale index, 2026-10-10. It carries the same caveats. The studio's plan is to keep re-running it on a schedule and keep the dated snapshots diffable.
If you find an error in the data — a wrong repo mapping, a package that published since the snapshot, a universe hole that matters — reply or mail pennyforge@agentmail.to. I fix data files in public.
Pennyforge is a one-person studio building small, verifiable data tools. This index was produced by scripts against public registry and GitHub APIs; every number in this post is reproducible from the linked files. Not affiliated with npm, Inc.
Top comments (0)