# npm Registry

> The npm registry is the public database at registry.npmjs.org that stores every npm package's metadata and tarballs and serves them to package managers.

Also known as: npmjs registry, registry.npmjs.org, node registry, public npm registry, npm package registry

Source: https://socket.dev/glossary/npm-registry

Last updated: 2026-10-30

The npm registry, also called the npmjs or node registry, is the public package database at registry.npmjs.org that stores every published version as an immutable tarball plus JSON metadata, and serves both to npm, pnpm, Yarn, and Bun. It authenticates publishers, not code: whatever a valid credential publishes stays installable until someone removes it.

Removal is the npm registry's real security boundary, and it is slow and quiet. The registry keeps a timestamp for every version it ever accepted, including versions it later deleted, so its own metadata records how long bad code stayed live. We read those timestamps on October 30, 2026. The malicious plain-crypto-js@4.2.1 behind the [axios compromise](https://socket.dev/blog/axios-npm-package-compromised) was installable for 3 hours 26 minutes. Packages from the [Beamglea phishing campaign](https://socket.dev/blog/175-malicious-npm-packages-host-phishing-infrastructure) lasted a median of 15 days. Seventeen more packages carrying the same Beamglea redirect script are still installable a year later.

Our position: treat the npm registry as a distribution network with a reactive abuse desk, not as a vetting layer. Publish-time malware scanning, live since July 28, 2026, narrows the gap without changing the model. The decision about what runs on your machine still has to be made at install time, from what is inside the tarball.

## What is the npm registry?

The npm registry is an HTTP API that maps package names to metadata documents and tarballs. npm's documentation calls it ["a database of JavaScript packages, each comprised of software and metadata."](https://docs.npmjs.com/about-the-public-npm-registry) The [npm CLI](https://socket.dev/glossary/npm-node-package-manager) is one client. pnpm, Yarn, and Bun read the same API, so a policy the registry enforces protects every client, while a setting in one client protects only that client's users.

The registry was designed in 2010 to make publishing nearly frictionless. Older npm documentation describes it as ["powered by a CouchDB database,"](https://docs.npmjs.com/cli/v8/using-npm/registry) and the CouchDB heritage still shows. Every package is one JSON document, every document carries a `_rev` revision, and anyone with an account can create a new name without human review.

GitHub has operated the npm registry since March 2020, when it [announced the npm acquisition](https://github.blog/news-insights/company-news/npm-is-joining-github/) and described npm as "home to over 1.3 million packages with 75 billion downloads a month." The API sits behind Cloudflare. Every response we fetched carried `server: cloudflare` and `cache-control: public, max-age=300`, so a metadata change can take up to five minutes to reach clients reading through the CDN.

"Node registry" is not a separate service. Node.js does not run a package registry. People searching for the node registry mean registry.npmjs.org, the default value of npm's `registry` setting.

## How many packages are on the npm registry?

The npm registry holds about 4.3 million installable package names, by our count on October 30, 2026. npm does not publish an official total, and the registry root returns an empty object:

```sh
$ curl -s https://registry.npmjs.org/
{}
```

We started from [all-the-package-names](https://github.com/nice-registry/all-the-package-names), a package that lists every public name and republishes daily. Version 2.0.2609, published at 00:14 UTC on October 30, 2026, lists 4,618,847 names. Of those, 1,831,822, or 39.7%, are scoped:

```sh
$ curl -sL https://registry.npmjs.org/all-the-package-names/-/all-the-package-names-2.0.2609.tgz \
    | tar xzO package/names.json \
    | jq 'length, (map(select(startswith("@")))|length)'
4618847
1831822
```

A name list overcounts, because a name survives after its code is gone. We fetched the packument for a random sample of 2,000 names to see what each one serves today:

| State on Oct 30, 2026 | Names in sample | Share | Projected across 4.62M names |
| --- | --- | --- | --- |
| Live, with installable versions | 1,876 | 93.8% | about 4.33 million |
| Security holding package (0.0.1-security) | 63 | 3.2% | about 145,000 |
| Unpublished tombstone, no versions | 37 | 1.9% | about 85,000 |
| HTTP 404 | 24 | 1.2% | about 55,000 |

The 95% confidence interval on the live share is about ±1.1 points, which puts the live count between roughly 4.28 and 4.38 million. Growth has been steep. npm's official statistics showed more than 2.5 million live packages at the end of 2023, and [our 2023 npm retrospective](https://socket.dev/blog/2023-npm-retrospective) counted 10,518,566 versions published and 1,241,583 unpublished that year alone. "Live" does not mean "legitimate." The live share includes spam, abandoned names, and packages nobody has reviewed.

## How the npm registry stores a package

The npm registry stores each package as one metadata document, called a packument, plus one tarball per version. Four packument fields decide what clients install:

- `versions` maps each version string to its manifest, including `dist.tarball`, `dist.integrity`, and the registry's signature.
- `dist-tags` maps labels such as `latest` to versions. A bare `npm install semver` resolves `latest`.
- `time` records when each version was published. Entries for deleted versions stay in this map.
- `_npmUser` on each version records which identity published it.

Clients usually request the abbreviated form with `Accept: application/vnd.npm.install-v1+json`, which keeps only the fields needed to install. For axios on October 30, 2026, the full packument was 868,092 bytes and the abbreviated one 305,314 bytes. The abbreviated form drops `time`, so the forensic record described below is invisible to a normal install.

Smaller endpoints expose the same data. We ran these against the live registry:

```sh
$ curl -s https://registry.npmjs.org/-/package/semver/dist-tags
{"legacy":"5.7.2","latest-6":"6.3.1","latest":"7.8.5"}

$ curl -s https://registry.npmjs.org/semver/7.8.5 | jq -c ._npmUser
{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:7a0c479c-0252-4ccd-81e5-c43546a76551"}}
```

The second response shows that semver 7.8.5 was published through [trusted publishing](https://socket.dev/glossary/npm-trusted-publishing) from GitHub Actions, not from a maintainer's laptop. That field records how a version was published, not whether its code is safe. In the [TanStack compromise](https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack), attackers published through the project's legitimate trusted-publisher binding.

Registry data is immutable at the version level. npm's [unpublish policy](https://docs.npmjs.com/policies/unpublish) states: "Once package@version has been used, you can never use it again." That rule is why a lockfile's integrity hash means something. The bytes behind a name and version can be deleted, but never swapped. Everything around them is mutable: `dist-tags` move, versions get deprecated or removed, and npm can rebuild the whole document.

> Figure 1. What the npm registry serves for a version in each state, and what npm install does. Observed against registry.npmjs.org with npm 10.9.9 on October 30, 2026.
>
> Lifecycle of a version on the npm registry: publish, publish-time scan, live, then deprecated, unpublished tombstone, or removed by npm and replaced with a security holding package.

Figure 1 in text. A new version first passes publish-time scanning, which GitHub says typically takes about five minutes and can take 15 minutes or more. It then becomes live: it appears in `versions` and `time`, its tarball returns HTTP 200, and ranges and `latest` resolve to it. From there a version can reach three end states. A deprecated version stays in `versions` and installs, and npm prints the deprecation message. An unpublished package becomes a tombstone: the full packument keeps `time.unpublished` with the removed version list, the abbreviated packument returns only `name` and `modified`, and `npm install` fails with `ENOVERSIONS`. A package removed by npm becomes a security holding package: npm deletes the versions, their tarballs return HTTP 404, a `0.0.1-security` placeholder owned by the `npm` account takes over `latest`, and the old publish dates stay in `time`. `npm install <name>` then succeeds and installs an empty placeholder.

## What happens when npm removes a malicious package?

When npm removes a [malicious package](https://socket.dev/glossary/malicious-package), it deletes the bad versions and their tarballs and publishes a placeholder called a security holding package. A malware advisory then appears in the [GitHub Advisory Database](https://socket.dev/glossary/github-advisory-database). The axios compromise in March 2026 shows each step. We fetched the packument for plain-crypto-js, the dependency the attacker injected into axios@1.14.1 and axios@0.30.4:

```sh
$ curl -s https://registry.npmjs.org/plain-crypto-js | jq '{"dist-tags", versions: (.versions|keys), time}'
{
  "dist-tags": { "latest": "0.0.1-security.0" },
  "versions": [ "0.0.1-security.0" ],
  "time": {
    "created": "2026-03-31T04:26:32.907Z",
    "modified": "2026-03-31T04:26:35.731Z",
    "4.2.0": "2026-03-30T05:57:32.867Z",
    "4.2.1": "2026-03-30T23:59:12.278Z",
    "0.0.1-security": "2026-03-31T03:25:11.636Z",
    "0.0.1-security.0": "2026-03-31T04:26:33.055Z"
  }
}
```

The malicious 4.2.1 is gone from `versions` but still listed in `time`. Its tarball URL returns 404, and `npm install plain-crypto-js@4.2.1` fails with "No matching version found." The first placeholder landed at 03:25:11 UTC, 3 hours 25 minutes 59 seconds after the malicious publish. The document's `created` timestamp is later than its own versions, which shows npm rebuilt the document rather than editing it. Socket's scanner flagged 4.2.1 at 00:05:41 UTC, six and a half minutes after publish.

A security holding package installs without error. With npm 10.9.9, `npm install plain-crypto-js` added `0.0.1-security.0`, a 438-byte package described as "security holding package", and reported "found 0 vulnerabilities." The advisory for the package covers `>=0`, and under semver rules that range does not match a prerelease such as `0.0.1-security.0`, so `npm audit` stays silent. The placeholder exists to stop anyone from re-registering the name, not to warn you. A project that silently resolves to it has lost a dependency without noticing.

The warning lives in the advisory feed. The bulk endpoint that `npm audit` calls returns a malware advisory for the removed version:

```sh
$ curl -s -X POST -H 'content-type: application/json' \
    https://registry.npmjs.org/-/npm/v1/security/advisories/bulk \
    -d '{"plain-crypto-js":["4.2.1"]}'
{"plain-crypto-js":[{"id":1115706,"url":"https://github.com/advisories/GHSA-2x9r-6wxq-hrr7",
  "title":"Malware in plain-crypto-js","severity":"critical","vulnerable_versions":">=0",
  "cwe":["CWE-506"],"cvss":{"score":0,"vectorString":null}}]}
```

The advisory is rated critical, with a CVSS score of 0 and no CVE ID. Malware does not get a [CVE](https://socket.dev/glossary/common-vulnerabilities-and-exposures-cve), which is why scanners that only match CVEs miss it. The advisory text is blunt: ["Any computer that has this package installed or running should be considered fully compromised."](https://github.com/advisories/GHSA-2x9r-6wxq-hrr7)

The holding-package convention is metadata, and metadata can be wrong. In June 2026, an [npm tooling bug marked one-character packages as security holders](https://socket.dev/blog/npm-tooling-single-character-bug). It moved `latest` on names such as `c`, `r`, and `x` to `0.0.1-security` until npm reverted the change the same day.

Owners can also remove their own packages. npm allows an unpublish within 72 hours of publishing. After that, a package qualifies only if no other public package depends on it, it had fewer than 300 downloads in the last week, and it has a single owner. The resulting tombstone does not say who unpublished the package or why. One tombstone from our sample, reply-mquery, returns this abbreviated document, identical in shape to any other:

```json
{"name":"reply-mquery","modified":"2026-03-01T19:42:35.271Z"}
```

## How long do malicious packages stay on the npm registry?

Malicious and abusive packages stay on the npm registry for hours to more than a year, depending on when someone notices them. Because `time` keeps entries for deleted versions, the gap between the first bad publish and the first `0.0.1-security` placeholder measures the exposure window. We computed it from live packuments on October 30, 2026.

> Figure 2. Time from first publish to the first 0.0.1-security placeholder, read from each packument's time map on October 30, 2026. Log scale.
>
> Log-scale chart of how long malicious and abusive packages stayed installable on the npm registry, from 3 hours 26 minutes for plain-crypto-js to more than a year for surviving Beamglea packages.

Figure 2 in text, as a table:

| Case | First publish | First placeholder | Installable for |
| --- | --- | --- | --- |
| plain-crypto-js@4.2.1, axios compromise | Mar 30, 2026, 23:59 UTC | Mar 31, 2026, 03:25 UTC | 3 h 26 min (Socket flagged it at 6 min 29 s) |
| crossenv, typosquat of cross-env | Jul 19, 2017 | Aug 1, 2017 | 13 days 11 hours |
| Beamglea-pattern phishing packages, 171 names | Sep 15 to Oct 8, 2025 | Oct 8 to 9, 2025 | median 15.3 days, range 1.5 to 24.1 days |
| Beamglea packages published after the takedown, 17 names | Oct 8 to 23, 2025 | none | still installable, 372 days or more |
| Spam names in our random sample, 31 names | May to Jul 2024 | Nov 12 to 13, 2025 | median 545 days, range 489 to 554 days |

crossenv is the textbook [typosquatting](https://socket.dev/glossary/typosquatting) case. It sent environment variables to a remote host at install time, and [npm's 2017 write-up](https://blog.npmjs.org/post/163723642530/crossenv-malware-on-the-npm-registry.html) said "the intent was to collect useful data from tricked users." Its packument still lists 15 publish timestamps from July 19, 2017, for versions that no longer exist.

The Beamglea packages did not run on install. As we documented in October 2025, the campaign used the registry as free hosting: package CDNs serve any public npm file over HTTPS, so a package named `redirect-` plus six random characters became a phishing redirector without the attacker running a server. We downloaded the 17 surviving tarballs. Every one ships a `beamglea.js` redirect script, and every one embeds a target's email address in its redirect URL. We are not naming them here, because they still work.

The 31 spam names make up about half of the holding packages in our random sample. They follow the generated naming style of the [TEA protocol spam waves](https://socket.dev/blog/tea-protocol-spam-floods-npm-but-its-not-a-worm), whose packages npm took down in mid-November 2025. Spam is not malware, but the cleanup shows the same pattern: the registry acts in bulk, after outside researchers raise the alarm.

The windows explain what a [dependency cooldown](https://socket.dev/glossary/dependency-cooldown) can and cannot do. A one-day minimum release age would have kept plain-crypto-js@4.2.1 out of every fresh resolution. A seven-day window would still have let in crossenv and most Beamglea packages, which stayed up longer. A cooldown protects you only when someone else finds and removes the version before your window closes.

## npm registry security controls compared

The npm registry has added controls on both sides of a publish since 2016. Each one closes a specific path, and each leaves a gap that a recent attack walked through.

| Control | Since | What it stops | What it misses |
| --- | --- | --- | --- |
| Unpublish limits | March 2016 | Owners deleting packages that others depend on | Malicious versions, which npm removes itself |
| Punctuation-only name rule | December 2017 | New names that differ from existing ones only by -, \_, or . | Other lookalikes, new brand names, and scope impersonation |
| Trusted publishing (OIDC) | 2025 | Stored publish secrets in CI | A compromised workflow publishing through the real binding |
| Classic token revocation | December 9, 2025 | Long-lived classic tokens harvested from CI | Granular tokens and stolen sessions |
| Reset of granular tokens that bypass 2FA | May 19, 2026 | Tokens the Mini Shai-Hulud worm had already collected | Tokens issued and stolen after the reset |
| Staged publishing (opt-in preview) | May 20, 2026 | A publish going live without a human 2FA approval | Every package whose owner has not turned it on |
| Publish-time malware scanning | July 28, 2026 | Malware that matches what the scanner recognizes | Novel payloads that pass the scan |
| Removal and security holding package | By 2017 | Further installs of a known-bad version | Every install during the exposure window |

Our verdict: staged publishing is the only registry control that puts a human between a stolen credential and an installable version, and it should be the default for any package with meaningful downloads. The other controls each harden one step and leave the rest to chance.

The punctuation rule arrived in December 2017, months after crossenv. In [npm's words](https://blog.npmjs.org/post/168978377570/new-package-moniker-rules), "you can no longer publish packages that differ from existing packages in punctuation only." It blocks crossenv-style names and nothing else. Trusted publishing, which [npm rolled out to all users in 2025](https://socket.dev/blog/npm-trusted-publishing), removes stored secrets but trusts whatever the workflow builds. The [classic token revocation](https://socket.dev/blog/npm-revokes-classic-tokens) and the [reset of 2FA-bypass granular tokens](https://socket.dev/blog/npm-invalidates-tokens-mini-shai-hulud), which followed the [Mini Shai-Hulud wave against @antv](https://socket.dev/blog/antv-packages-compromised), cut off credentials attackers had already collected. [Staged publishing](https://socket.dev/blog/npm-to-implement-staged-publishing) entered public preview the next day in npm CLI 11.15.0, and it is still opt-in per package.

Publish-time scanning has the clearest limit. GitHub's [changelog](https://github.blog/changelog/2026-07-28-npm-publish-time-malware-scanning-and-dual-use-metadata/) says each new version may be "published as normal, held for manual review, or blocked." One week after launch, on August 4, 2026, the [keyv and cacheable compromise](https://socket.dev/blog/popular-npm-packages-in-the-keyv-and-cacheable-namespaces-compromised-in-active-supply-chain) shipped a malicious preinstall hook through the registry anyway. None of these controls inspects what the code does once it is installed.

## Registry configuration, scopes, and proxies

npm installs from the URL in its `registry` setting, which defaults to `https://registry.npmjs.org/`, unless a package's scope is mapped elsewhere. Scoped mappings in `.npmrc` route individual namespaces to private registries:

```ini
registry=https://registry.npmjs.org/
@acme:registry=https://npm.acme.internal/
```

Unscoped internal names are where this goes wrong. Suppose an internal package called `acme-billing` is unscoped and the public registry also has a package by that name. A client or proxy that checks both sources can pick the public one, which is [dependency confusion](https://socket.dev/glossary/dependency-confusion). Scope every internal package and map the scope explicitly.

The `registry` setting is also where a [package firewall](https://socket.dev/glossary/package-firewall) plugs in: a proxy between your clients and registry.npmjs.org that decides, version by version, whether to serve a package. That moves the install decision off the public registry's removal clock. Since early 2026, npmjs.com package pages have also [linked to Socket's analysis](https://socket.dev/blog/socket-security-analysis-is-now-one-click-away-on-npm) of each package, which puts a code-level review one click from the registry's own page.

## What the npm registry should change next

The npm registry already records exposure windows, but only in a field normal installs never request. Removal leaves no machine-readable reason and no link from the placeholder to its advisory. A tombstone looks the same whether an owner cleaned up a test package or npm pulled malware. We think every removal should add a `removed` record to both the full and abbreviated packuments, with a timestamp, a reason code, and the advisory ID. Clients could then explain a removal, instead of failing with a bare 404 on a pinned tarball or quietly resolving an unpinned range to an empty placeholder with zero vulnerabilities reported.

The deeper problem is the design goal. The registry was built to make publishing frictionless, and it still accepts whatever an authenticated publisher sends. Seventeen packages carrying the redirect script from a campaign we documented a year ago are still being served. Until removal is fast and complete, the registry's verdict on a package is the last one to arrive, so yours has to come first.
