This is a submission for DEV's Summer Bug Smash: Clear the Lineup.
The bug
npmx.dev is a fast browser for the npm registry — you
can look up any package or compare two side by side. On the compare
page, package version numbers were sometimes just... wrong. Not
crashing-wrong, just quietly stale — showing a version that npm had
long since replaced.
The bug had already been reported (issue #1832):
comparing tinyclip and copy-paste showed tinyclip stuck at its
very first published version, 0.0.1, when the real latest was
0.1.8. The original screenshots in that issue show the mismatch
clearly next to what the npm registry itself reports.

The original bug, as reported in issue #1832 — tinyclip stuck at v0.0.1 instead of the real latest, v0.1.8.
The kind of bug that's easy to shrug off as "just a display issue" —
but on a site whose entire purpose is showing accurate package data, a
silently wrong version number undermines the one thing the tool exists
to do.
Chasing it down
My first assumption was that the comparison logic itself was buggy —
maybe it was reading the wrong field, or resolving latest incorrectly.
That assumption was wrong, and figuring out why it was wrong is most
of this story.
Tracing the data flow: the compare page calls a composable called
usePackageComparison, which for each package calls $npmRegistry(...)
to fetch that package's full registry data — including which version is
tagged latest. That function is a thin wrapper around another
composable, useCachedFetch().
That's where it got interesting. useCachedFetch() takes a ttl
(time-to-live) argument — a signal for how long a cached response
should be considered fresh. Except on the client, the parameter was
named _ttl:
_ttl: number = FETCH_CACHE_DEFAULT_TTL,
The underscore prefix is a convention for "this parameter is
intentionally unused." It was accepted, and then never referenced again
in the function body. Whatever caching strategy the ttl value was
supposed to control, it wasn't actually controlling anything on the
client.
Instead, the client branch hardcoded this:
const defaultFetchOptions: Parameters<typeof $fetch>[1] = {
cache: 'force-cache',
}
force-cache is a browser fetch option with a specific meaning: if any
cached response already exists for a URL, use it — don't check whether
it's still fresh. That's different from the browser's normal caching
behavior, which checks Cache-Control headers and revalidates once a
response goes stale.
So once a browser had fetched a package's registry data one time —
whenever that first happened, possibly weeks earlier — it would keep
serving that exact same response indefinitely. New versions could be
published to npm and the app would never know, because the browser was
never allowed to ask again.
I checked what the npm registry actually sends back, to confirm this
wasn't just a theory:
cache-control: public, max-age=300
etag: "..."

The registry's actual response headers — it was already telling clients how to cache correctly.
The registry is already telling clients exactly how to cache correctly
— treat this as fresh for 5 minutes, then use the ETag to check back
efficiently. The app just wasn't listening to that instruction.
The fix
The fix itself is small — which is sometimes a sign you've found the
actual root cause rather than papered over a symptom. In
app/composables/useCachedFetch.ts, both places that hardcoded
cache: 'force-cache' (the client branch, and a server-side fallback
branch with identical logic) now use:
const defaultFetchOptions: Parameters<typeof $fetch>[1] = {
cache: 'default',
}
'default' tells the browser to follow standard HTTP caching rules —
respect the registry's max-age, and revalidate via ETag once it
expires, rather than assuming a response is correct forever regardless
of age.
PR: https://github.com/npmx-dev/npmx.dev/pull/3156
Verifying it
- Ran the existing test suite — all 1,000+ unit tests across 55+ files passed with zero failures, confirming the change doesn't break anything else relying on this composable
- Loaded the compare page against the fix and confirmed it renders cleanly with no console errors
- Inspected the request in DevTools and confirmed a full, real network round-trip completes end to end (DNS lookup, connection, waiting for server response, content download) rather than resolving instantly — consistent with the browser actually being allowed to talk to the registry instead of only ever reading a frozen local copy

A full network round-trip completing after the fix — not an instant cache hit.
What I'd take from this
The most useful signal in this whole investigation wasn't a stack trace
or an error message — it was an unused function parameter. _ttl was
evidence that someone had designed a caching contract around a real TTL
concept, and a later implementation detail (hardcoding force-cache)
quietly bypassed it without anyone updating the signature to match.
Dead parameters are worth pausing on — they're often a fossil record of
intent the code no longer honors.
Fixes npmx-dev/npmx.dev#1832 via PR #3156
Top comments (0)