# Dependency Cooldown (Minimum Release Age)

> A dependency cooldown makes a package manager skip versions published less than a set time ago, giving malicious releases time to be caught and removed.

Also known as: minimumReleaseAge, min-release-age, minimum publish age, min-publish-age, release age gate, npmMinimalAgeGate, exclude-newer, cooldown period, cool-down policy

Source: https://socket.dev/glossary/dependency-cooldown

Last updated: 2026-10-29

A dependency cooldown, also called a minimum release age or minimum publish age, is a resolver rule that refuses any package version published more recently than a set window, such as 24 hours or 7 days. Most malicious releases are detected and pulled within hours, so a cooldown lets detection finish before your install runs.

Cooldowns work because most supply chain attacks are short. In the [axios compromise](https://socket.dev/blog/axios-npm-package-compromised), the payload-carrying dependency plain-crypto-js@4.2.1 was live on npm for 3 hours and 26 minutes, according to the registry's own timestamps. Socket's scanner flagged it 6 minutes and 29 seconds after it was published. A 24-hour cooldown would not have accepted it until more than 20 hours after npm had already replaced it.

We think every package manager should enable a cooldown by default. As of October 2026, [pnpm 11](https://socket.dev/blog/pnpm-11-adds-new-supply-chain-protection-defaults) and Yarn 4.15 have turned one on by default, and [npm 12](https://socket.dev/blog/npm-12) has not. We also think teams misread what a cooldown is. A cooldown is a clock, not a detector. It protects you only if someone analyzes the package's actual contents during the window, and it does nothing about malware that stays hidden longer than the window.

## What is a dependency cooldown?

A dependency cooldown is a filter applied during version resolution. Before picking a version, the resolver drops every candidate whose publish timestamp is newer than the current time minus the window.

To see why that matters, start with what a version range means. A [semantic versioning](https://socket.dev/glossary/semantic-versioning-semver) range like `^1.14.0` does not name a version. It names a set of versions that grows every time someone publishes to the package. The package manager resolves the range against whatever the registry holds at the moment you run the command. Resolution is a function of your manifest and the registry's state at install time.

That design is deliberate. Ranges let a maintainer's bug fix reach every dependent without anyone editing a manifest, and they let package managers deduplicate shared [transitive dependencies](https://socket.dev/glossary/transitive-dependency). The same property lets a hijacked publish reach every dependent without anyone editing a manifest. Nothing in the range distinguishes a fix from a payload.

The registry already records the information a cooldown needs. The full npm package document, called the packument, carries a `time` map with a publish timestamp for every version:

```bash
curl -s https://registry.npmjs.org/axios | jq '.time | {"1.14.0", "1.14.1", "1.20.0", "1.21.0"}'
```

```json
{
  "1.14.0": "2026-03-27T19:01:40.915Z",
  "1.14.1": "2026-03-31T00:21:58.168Z",
  "1.20.0": "2026-08-26T08:20:14.517Z",
  "1.21.0": "2026-10-27T07:18:54.313Z"
}
```

The `time` map keeps entries for versions that no longer exist. Version 1.14.1 is one of the two malicious axios releases. It is gone from `versions`, but its publish time is still in `time`, which makes the map useful for reconstructing an incident after the fact.

npm clients do not normally fetch this document. They fetch the abbreviated "corgi" form (`Accept: application/vnd.npm.install-v1+json`), whose top-level keys are only `dist-tags`, `modified`, `name`, and `versions`. Every cooldown on npm therefore has to download the full packument instead. For axios that was 868 KB uncompressed versus 305 KB for the abbreviated form when we measured it on October 29, 2026. That extra fetch is the real cost of a cooldown on npm, and it is why pnpm's maintainers paired [pnpm 10.16's minimumReleaseAge](https://socket.dev/blog/pnpm-10-16-adds-new-setting-for-delayed-dependency-updates) with more aggressive metadata caching.

Here is the filter in action with npm 12.2.1, run on October 29, 2026 at 04:44 UTC, when axios 1.21.0 was about 45 hours old:

```console
$ cat package.json
{ "name": "demo", "version": "1.0.0", "dependencies": { "axios": "^1.14.0" } }

$ npm install --package-lock-only --ignore-scripts
$ grep -A1 '"node_modules/axios"' package-lock.json
    "node_modules/axios": {
      "version": "1.21.0",

$ echo 'min-release-age=7' > .npmrc
$ npm install --package-lock-only --ignore-scripts
$ grep -A1 '"node_modules/axios"' package-lock.json
    "node_modules/axios": {
      "version": "1.20.0",
```

The range did not change. The cutoff moved to October 22, 04:44 UTC, so 1.21.0 dropped out of the candidate set and the resolver picked the newest version that remained. The same filter applies to every transitive dependency in the tree, not just the one you named.

## The exposure window is hours, not days

Dependency cooldowns work because attackers need fast adoption and defenders now detect fast. A compromised publish does damage only between the moment it goes live and the moment the registry removes it. A cooldown longer than that interval means the malicious version is never eligible for your install.

The axios incident shows the shape precisely. We reconstructed this timeline from the npm registry's `time` fields for axios and plain-crypto-js, plus the detection time in [our report on the compromise](https://socket.dev/blog/axios-npm-package-compromised):

> Timeline of the axios and plain-crypto-js compromise on March 30 and 31, 2026, with the times a 24-hour and a 7-day dependency cooldown would first have accepted the malicious version

| Time (UTC) | Event | Source |
| --- | --- | --- |
| Mar 30, 05:57:32 | plain-crypto-js@4.2.0 published, a clean copy of crypto-js | npm time |
| Mar 30, 23:59:12 | plain-crypto-js@4.2.1 published with a postinstall dropper | npm time |
| Mar 31, 00:05:41 | Socket flags 4.2.1 as malware | Socket report |
| Mar 31, 00:21:58 | axios@1.14.1 published, adding plain-crypto-js as a dependency | npm time |
| Mar 31, 01:00:57 | axios@0.30.4 published, poisoning the 0.x branch | npm time |
| Mar 31, 03:25:11 | npm replaces plain-crypto-js with a 0.0.1-security placeholder | npm time |
| Mar 31, 23:59:12 | Earliest moment a 24-hour cooldown would accept 4.2.1 | computed |
| Apr 6, 23:59:12 | Earliest moment a 7-day cooldown would accept 4.2.1 | computed |

The malicious payload was live for 3 hours and 26 minutes. Socket flagged it 16 minutes before axios@1.14.1 even existed. Detection was not the bottleneck in this attack. Adoption speed was: any project with `^1.14.0`, and any `npx` run that resolved axios through a dependency, could pull the payload within minutes of publication.

The pattern holds across ecosystems. In the [Rust crates compromise](https://socket.dev/blog/popular-rust-crates-compromised) in August 2026, the Rust Security Response Team reported that the malicious arrayref, internment, and append-only-vec releases were available on crates.io for 86, 90, and 107 minutes, as cited in our coverage of [Cargo's minimum publish age](https://socket.dev/blog/rust-cargo-cooldowns). In the [TanStack compromise](https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack), Socket flagged all 84 malicious package artifacts within six minutes of publication.

William Woodruff analyzed ten well-known open source supply chain attacks and found that eight had windows of opportunity shorter than a week. He estimated a 7-day cooldown would have stopped most of them from reaching users. His conclusion, from [the post making the case for cooldowns](https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns):

"an 80-90% reduction in exposure through a technique that is free and easy seems hard to beat." (William Woodruff, November 2025)

Worms make the argument stronger. A self-propagating worm like [Mini Shai-Hulud](https://socket.dev/blog/npm-invalidates-tokens-mini-shai-hulud) steals publish credentials from a CI runner or developer machine and republishes every package that victim maintains under the `latest` tag. Each new generation of the worm needs someone to install a freshly published version. By May 21, 2026, Socket had tracked 1,055 compromised versions across 502 packages in that campaign. A cooldown longer than detection time breaks the chain: each infected version is reported and removed before it becomes eligible on machines that enforce the window.

## How do you set a minimum release age in pnpm or npm?

To set a minimum release age in npm, add `min-release-age=7` to `.npmrc`; the value is in days, and [npm 11.10.0 introduced min-release-age](https://socket.dev/blog/npm-introduces-minimumreleaseage-and-bulk-oidc-configuration). To set one in pnpm, add `minimumReleaseAge: 10080` to `pnpm-workspace.yaml`; the value is in minutes, and pnpm 11 and later default to 1440, which is 24 hours.

```ini
# npm 11.10.0+: days
min-release-age=7
# npm 11.17.0+: names or globs that skip the window
min-release-age-exclude[]=@myorg/*
```

```yaml
# pnpm 10.16+: minutes. pnpm 11+ defaults to 1440.
minimumReleaseAge: 10080
minimumReleaseAgeExclude:
  - '@myorg/*'
  - typescript@5.9.3   # one vetted version, not every future one
```

Most major package managers now have the setting, but the names, units, and defaults all differ. Go, Maven, NuGet, and Composer still have no built-in equivalent. We checked each one against its documentation or by running it in October 2026:

| Tool | Setting | Unit | Default | Exemptions |
| --- | --- | --- | --- | --- |
| npm 12.2.1 | min-release-age in .npmrc | days | off | min-release-age-exclude (names, globs) |
| pnpm 12.16.1 | minimumReleaseAge in pnpm-workspace.yaml | minutes | 1440, non-strict | minimumReleaseAgeExclude (names, globs, versions) |
| Yarn 4.18.1 | npmMinimalAgeGate in .yarnrc.yml | duration; a bare number is minutes | 1d | npmPreapprovedPackages |
| Bun 1.4.4 | minimumReleaseAge under \[install\] in bunfig.toml | seconds, or a duration like 3d | none observed | minimumReleaseAgeExcludes (names, exact versions) |
| pip 26.2.1 | --uploaded-prior-to | ISO 8601 duration like P7D, or a datetime | off | none |
| uv | exclude-newer | "7 days", ISO 8601, or RFC 3339 | off | exclude-newer-package |
| Cargo 1.100 (due Nov 12, 2026) | registry.global-min-publish-age in .cargo/config.toml | "7 days" and similar | "0" (off) | per-registry setting, env override |
| Bundler 4.0.13+ | cooldown: on a Gemfile source, or --cooldown | days | off | per-source value, --cooldown 0 |

The unit mismatch is a real trap. The value `7` means 7 days in npm, 7 minutes in pnpm, and 7 seconds in Bun. A config line copied from one tool to another can shrink the window by a factor of 1,440 or 86,400 without any warning.

Misspellings were silent too. A typo in `minimumReleaseAge` dropped the policy with no warning until [pnpm 12](https://socket.dev/blog/pnpm-12) started reporting unrecognized settings in `pnpm-workspace.yaml`.

The tools also disagree on where the window lives. npm and pnpm both read user-level config, so one line in `~/.npmrc` or pnpm's global `config.yaml` covers every project on the machine. Cargo applies the window per registry. [Bundler's cooldown](https://socket.dev/blog/rubygems-adds-bundler-cooldown) applies per Gemfile source, which lets an internal gem server skip the delay while RubyGems.org keeps it. [Automated dependency update](https://socket.dev/glossary/automated-dependency-updates) bots have their own cooldown settings, which usually apply only to the direct dependency they bump. When a bot regenerates the lockfile without honoring the package manager's window, it can commit a transitive entry that the package manager later rejects in CI.

## Fallback versus deny when nothing is old enough

When no version satisfies both the range and the window, a cooldown either falls back to a too-new version or fails resolution. That choice decides whether the cooldown is a security control or a preference.

An exact pin to a version inside the window fails on every tool we tested. Here is npm 12.2.1 with `min-release-age=7` and `"axios": "1.21.0"`:

```console
$ npm install --package-lock-only
npm error code ETARGET
npm error notarget No matching version found for axios@1.21.0 with a date before 10/22/2026, 4:44:49 AM.
```

pnpm 12.16.1 with an explicit `minimumReleaseAge` fails the same way, naming the publish time and the cutoff. Yarn 4.18.1 reports that "all versions satisfying" the pin "are quarantined." A range with an older match falls back silently to that older version, which is the behavior you want.

The hard case is a range where the only remaining version is the new one. Cargo's design answers it with `resolver.incompatible-publish-age = "deny"`. Its [RFC](https://rust-lang.github.io/rfcs/3923-cargo-min-publish-age.html) explains why: an attacker with publish rights could release a malicious version and yank the safe ones, and a fallback policy would then select the malicious version. That attack is not hypothetical. In the Rust crates compromise, the attacker yanked older legitimate arrayref releases, which could steer new resolutions toward the malicious 0.3.10.

> Two cooldown designs facing the same attack: the attacker publishes a malicious version and yanks every older version. A fallback cooldown installs the malicious version; a deny cooldown fails resolution.

The figure shows the same attack against both designs. The attacker publishes arrayref 0.3.10, a malicious release two hours old, and yanks 0.3.7, 0.3.8, and 0.3.9. Under a fallback policy, nothing old enough remains, so the resolver falls back and installs 0.3.10. Under a deny policy, no candidate passes the cutoff, so resolution stops with an error and nothing is installed. The version numbers match the real arrayref incident; the two-hour age and the exact set of yanked versions are illustrative.

pnpm's own documentation is explicit about its split. When you set `minimumReleaseAge` yourself, `minimumReleaseAgeStrict` defaults to `true` and pnpm fails resolution. The built-in 1440-minute default "is non-strict for backward compatibility," so it falls back to a too-new version when nothing older matches. We think that default should become strict. A fallback cooldown protects you from the common case and abandons you in exactly the case an attacker can engineer.

## Lockfiles, npx, and cooldowns cover different gaps

A [lockfile](https://socket.dev/glossary/lockfile) freezes versions you already resolved. A cooldown filters versions you are about to resolve. They cover different gaps, and each tool handles the overlap differently.

npm does not re-check locked versions against the window. We locked axios 1.21.0 with no cooldown, then set `min-release-age=7`. Both `npm install` and `npm ci` kept 1.21.0. A malicious version that entered your lockfile before you turned the cooldown on stays there. Cargo behaves the same way by design: its resolver keeps versions already recorded in `Cargo.lock`.

pnpm 12 does verify the lockfile. The same experiment under `pnpm install --frozen-lockfile` fails before installing anything:

```console
$ pnpm install --frozen-lockfile
✗ Lockfile failed supply-chain policy check (27 entries in 125ms)
Error: ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION
  ╰─▶ 2 lockfile entries failed verification:
        ...
        follow-redirects@1.16.2 was published at 2026-10-25T21:32:25.963Z,
      within the minimumReleaseAge cutoff (2026-10-22T04:51:40.822Z)
```

The output is trimmed to one of the two rejected entries.

That check catches a lockfile someone committed with a policy bypass. It also produces the failure people search for when they want to disable pnpm's minimum release age: a bot regenerates the lockfile with a dependency published hours earlier, and CI rejects it. The fix is an exact-version exemption, or waiting out the window and re-running CI, not `minimumReleaseAge: 0`.

Ad hoc execution is where a cooldown earns its keep. As [the hidden blast radius of the axios compromise](https://socket.dev/blog/hidden-blast-radius-of-the-axios-compromise) showed, [npx](https://socket.dev/glossary/npx-npm-package-runner) and global installs resolve fresh against the registry and never consult a project lockfile. A clean lockfile checked after the fact does not prove those runs were safe. A user-level cooldown does reach them. With `min-release-age=7` in the user config and no project at all, `npm exec --package=axios@^1.14.0`, the command behind `npx`, installed 1.20.0 into the npx cache, not 1.21.0.

The weakness is precedence. Project config overrides user config. In our test, a repository `.npmrc` containing `min-release-age=0` silently disabled a 7-day window set in the user's `~/.npmrc`. Any repository you clone can turn your cooldown off.

| Situation | Lockfile | Client cooldown | Cooldown enforced in the fetch path |
| --- | --- | --- | --- |
| Reinstall from an existing lockfile | Pins versions | npm keeps locked versions; pnpm 12 re-verifies them | Applies to each version fetched over the network |
| Adding or updating a dependency | No protection | Filters new resolutions | Filters new resolutions |
| npx and global installs | No protection | Only if set in user config | Applies |
| Repo .npmrc sets the window to 0 | Not affected | Disabled | Not affected |
| Git and URL dependencies | Pins the resolved commit or URL | No timestamp, so not covered | Depends on the source |

Verdict: use all three. Commit a lockfile and install with `npm ci` or `--frozen-lockfile` for reproducibility. Set a client cooldown at the user level, not only per project, so fresh resolutions are filtered. Enforce the window somewhere project config cannot reach when a cloned repository could undo it.

That last layer is where Socket enforces it. Socket's [cool-down policy](https://docs.socket.dev/docs/cool-down-policy) raises a `recentlyPublished` alert for any version younger than an org-wide threshold of 0 to 365 days, and Socket Firewall blocks the request when the policy action is `error`. The threshold lives in the org's Socket settings, not in any project file, and it covers npm, PyPI, Maven, Cargo, RubyGems, NuGet, Go, Conda, and Open VSX, using each registry's own publish timestamp. A [package firewall](https://socket.dev/glossary/package-firewall) cooldown has the same limits as any other cooldown, plus one more: it only sees traffic routed through it.

## How do you ship an urgent security fix during a cooldown?

To ship an urgent security fix during a cooldown, exempt the specific fixed version, install it, and remove the exemption once the version ages past the window. Do not disable the window, and do not exempt the package name.

The conflict is real. On July 29, 2026, the Rails security team published a critical Active Storage advisory, and a Bundler maintainer pointed out that projects with a cooldown would need `bundle update rails --cooldown 0` to pull the patch immediately, as we noted in our coverage of [Bundler 4.0.18](https://socket.dev/blog/bundler-extends-cooldown-to-bundle-lock-and-bundle-cache). npm handles the same case loudly. If the window blocks `npm audit fix` from installing a patched version, npm keeps the vulnerable version, warns, and exits non-zero.

Each tool's escape hatch has a different blast radius:

- **Bun and pnpm** accept exemptions for exact versions, such as `typescript@5.4.5`. A newer, unvetted release of the same package still waits out the window.
- **npm** matches `min-release-age-exclude` against package names and globs only. An exemption for `left-pad` covers every future version of `left-pad`, including one an attacker publishes tomorrow.
- **Cargo** overrides per invocation: `CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow cargo update -p some-crate --precise 1.2.3`. Once the version lands in `Cargo.lock`, later builds keep it under the normal policy.
- **Bundler** uses `--cooldown 0`, which disables the window for that whole run.

Name-wide exemptions for internal scopes are common and carry a second risk. npm's documentation warns that excluding a package "does not change which registry it is fetched from." If you exempt `@myorg/*` but do not own the `@myorg` scope on the public registry, an attacker can publish there and your cooldown will wave the package through. That is [dependency confusion](https://socket.dev/glossary/dependency-confusion) with the safety check pre-disabled.

## What a dependency cooldown does not stop

A dependency cooldown does not stop malware that stays undetected longer than the window. The [xz-utils backdoor](https://socket.dev/blog/how-to-use-socket-to-find-out-if-you-were-affected-by-the-backdoored-xz-package) had a window of about five weeks in Woodruff's analysis, longer than any cooldown a team would tolerate.

A cooldown also creates no detection of its own. The window is only valuable if something inspects the release while it waits. A CVE feed will not: malware does not get a CVE, and a freshly compromised version has no advisory until someone analyzes its contents. A cooldown paired with CVE scanning alone is a delay with nobody watching.

Several sources fall outside the clock entirely:

- **Sources without timestamps.** [Git and URL dependencies](https://socket.dev/glossary/git-dependencies) carry no registry publish time, so npm, pnpm, and Cargo cannot apply the window to them. Bundler goes further and lets through any version it cannot prove is too new, including versions from private gem servers without timestamp metadata.
- **Runtime fetches.** A package that downloads its real payload from a URL after install is as old as its tarball. The code it fetches is not.
- **Ambiguous timestamps.** On PyPI, a maintainer can upload new files to an existing release, so uv compares the upload time of each artifact rather than the release date. RubyGems allows new platform builds under an existing version number, which is why Bundler keys its cooldown on name and version only. Otherwise an attacker could push a fresh native build under an already-aged version and skip the window.

The strongest critique is economic. Cal Paterson [argues](https://calpaterson.com/deps.html) that cooldowns turn users into free riders: someone without a cooldown has to install the malicious version first so that someone else notices. He proposes registry-side upload queues instead. [gem.coop's cooldown beta](https://socket.dev/blog/gem-coop-tests-dependency-cooldowns) tested a version of that idea with a 48-hour delayed endpoint.

He is right that someone has to look before anyone installs. He is wrong that it has to be a victim. In the axios incident, the first look was automated content analysis that ran six and a half minutes after publication, before any meaningful install base existed. Cooldowns are sustainable as long as detection stays faster than the window. The open problem is that most registries still publish first and scan later, if at all, so the window is only as safe as the analysis someone else chooses to run inside it.
