I maintain a small actor that normalizes npm/PyPI/crates.io metadata into one row per dependency, and last week I added a "when was this last touched" column. Then I tried to turn that into "is this thing dead," which felt like it should be the same feature. It is not the same feature, and the three registries don't even agree on what "dead" would look like.
npm has an opinion field. Most packages leave it blank.
npm's deprecated is real — a string on the version object, set explicitly by the maintainer, and it shows up as a scary yellow warning the moment someone runs npm install:
curl -s https://registry.npmjs.org/istanbul | python3 -c "
import json, sys
d = json.load(sys.stdin)
v = d['dist-tags']['latest']
print(d['versions'][v].get('deprecated'))
"
# "This module is no longer maintained, try this instead:\nnpm i nyc\n..."
jade (renamed to pug) and gulp-util (replaced piecemeal) both set it too, both with a last publish back in 2015–2016. That's the field working as intended: opt-in, honest, and visible at install time.
Then there's bower. Last published March 2022, its own README calling it unmaintained — and the deprecated field is just... absent. Nobody who could set it ever did. The flag only fires if someone bothers to pull the trigger, and plenty of genuinely dead packages never get that courtesy.
The weirder case is colors, of 2022-supply-chain-incident fame. The sabotage versions (1.4.1, 1.4.2) are sitting right there in the registry's own time history — but dist-tags.latest was rolled back to 1.4.0 from 2019 and never moved. A plain npm install colors today quietly gets you the pre-incident version. The bad versions still exist, still installable if you pin them by number, just no longer the default anyone lands on by accident.
PyPI has the data. It just never revisits it.
PyPI ships a Development Status classifier — 1 - Planning through 7 - Inactive — that sounds exactly like what I wanted:
curl -s https://pypi.org/pypi/nose/json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d['info']['version'], [c for c in d['info']['classifiers'] if 'Development Status' in c])
"
# 1.3.7 ['Development Status :: 5 - Production/Stable']
nose hasn't shipped since June 2015 — eleven years, superseded by pytest for most of that time — and PyPI still calls it "Production/Stable." I checked requests, which released a new version this year, expecting a different string as a sanity check. Same one, word for word: 5 - Production/Stable. The classifier is text a maintainer typed once, at whatever release they felt like typing it at, and PyPI has no mechanism that ever revisits it. An actively-shipping project and an eleven-year corpse are indistinguishable on this specific field.
crates.io has neither field — but the timestamp underneath is the most honest of the three
The sparse index — the flat file cargo itself reads on every resolve, and the only crates.io endpoint my sandbox could reach this week — carries no status concept at all:
curl -s https://index.crates.io/ru/st/rustc-serialize | tail -1 | python3 -m json.tool
# keys: name, vers, deps, cksum, features, yanked, pubtime
No deprecated, no classifier, not even a homepage to go check by hand. What it does have is pubtime — and I almost got this part wrong. rustc-serialize 0.3.25, a crate long superseded by serde, shows pubtime: 2023-12-01. My first instinct was "that's obviously a backfill artifact, crates.io added this field at some point and stamped old versions with the migration date." That's a clean, confident, wrong guess: crates.io's own documentation says pubtime is backfilled without gaps but always holds the original publish time, and a second check confirmed 0.3.25 really was a genuine bugfix release, shipped eight years after the crate's heyday, in December 2023. The field was right. I wasn't.
So crates.io gives you the cleanest possible timestamp — precise, guaranteed present, verifiably not a backfill placeholder — and zero opinion about whether that timestamp is fine or alarming. You get better data and a harder job: you have to decide "is this stale" yourself, every time, with no maintainer's flag to lean on either way.
Where I'd push back on myself
Nine examples is a vibe check, not a survey — I picked packages I already knew the status of, which is exactly the sampling bias that makes a finding look cleaner than it is. I don't know what fraction of genuinely abandoned npm packages carry a deprecated flag versus staying silent like bower; that would take a much larger, unbiased sample I don't have the reachable endpoints to run this week. And "PyPI's classifier never changes" is what I observed on two packages — plausible given how the field works, but I haven't fetched enough dead PyPI projects to call it a rule rather than a pattern.
This staleness check is what I'm trying to fold into Package Registry Scraper next — a normalized "last touched" + "explicitly flagged" column instead of three registry-specific quirks to remember. The full field-by-field breakdown, with more examples, is in the cheatsheet: noble-ronin/package-staleness-data.
So: when you're deciding whether to add a dependency, do you actually check any of this, or do you go by download counts and vibes like most of us? And has a registry's own "stable" label ever actively misled you about something that turned out to be abandoned?

Top comments (1)
The split running under all three registries is claims versus observations. deprecated is a claim, and the person who has to make it is the one who already stopped caring enough to publish. The PyPI classifier is a claim typed once and never revisited. pubtime is not a claim at all. The registry observes it as a side effect of doing its job, and nobody has to remember anything for it to stay true. That is why the field with no opinion is the only one you can lean on, and I think your line about better data and a harder job is the finding rather than the consolation prize.
The rustc-serialize correction is the part I would keep longest. You guessed backfill, checked, and were wrong in the direction that would have made the column quietly wrong for every old crate while looking perfectly reasonable. A clean confident guess that nobody checks is worse than a messy one.
Where it lands for me is that any status signal has to be derived from something the subject does not author. pubtime is one. A couple more are visible to a mirror without asking anyone: whether the latest version still resolves against a current runtime, and how long open issues sit with no maintainer touching them. Both are behaviour rather than assertion, and neither can be set once and forgotten.
Both are also far more expensive than reading a field, which is the honest stopping point. You wanted one row per dependency, computed cheaply, at registry scale. Behavioural signals do not give you that. They give you a slow answer for the forty packages you actually depend on. That might be the real shape of the problem, but it is not the column you set out to add, and I would not pretend the substitution is free.