In July, I compared Composer and Packagist with five other ecosystems and found several gaps. One of them was easy to state: pnpm, Yarn, Bun, npm, uv, pip and Poetry all hold back freshly published releases on request, pnpm 11 even by default. Composer did not. The roadmap of 27 May named minimum-release-age as the first policy to follow the Dependency Policy Framework of Composer 2.10, and immutable versions on Packagist.org, live since 7 July, removed its last prerequisite.
Composer 2.11, released on 29 October 2026, delivers that policy under a different name: cooldown. Not quite four months from prerequisite to release is quick for a change that touches the resolver, the lock file and every command that picks a version.
A second change sits right below it in the changelog. Custom dependency policies, part of the policy model Andrew Nesbitt compared to uBlock Origin, can now stop composer install as well, not only composer update. For organisations that maintain their own lists, the policy now reaches the step that deploys to production.
Waiting buys time for someone else to look
Every cooldown rests on one observation: most malicious releases get found and pulled within hours or days. A version that has survived a week in public has survived the window in which most attacks get caught. Andrew Nesbitt's survey of cooldowns across package managers cites an analysis by William Woodruff in which eight of the ten supply chain attacks he examined had a window of less than a week.
The cooldown detects nothing itself. It buys time for those who do: Aikido, Socket, security researchers, and the maintainer who notices a release they never published. Nils Adermann's release post puts the numbers lower than Woodruff: even an hour gives automated scanners time to act, and manual review may need a few hours up to a day. A cooldown guarantees none of this. It only raises the odds.
In Composer 2.11, the cooldown sits under config.policy, next to advisories, malware and abandoned:
{
"config": {
"policy": {
"cooldown": {
"period": "7 days"
}
}
}
}
period takes a number and a unit (second, minute, hour, day or week, singular or plural, lower case) or an integer number of seconds. Composer rejects relative phrases such as "tomorrow". The default is null, and without a period the policy does nothing. block defaults to true. ignore uses the same format as the other policies, so every exception can carry a reason and a version constraint. Set audit to report or fail, and composer audit also lists installed versions that are still inside the cooldown; the default is ignore.
For a single run, a flag and two environment variables take precedence. --no-cooldown on update, require, remove and create-project lifts the cooldown alone and, unlike --no-blocking, leaves advisory, malware and custom list blocking in place. COMPOSER_POLICY_COOLDOWN_PERIOD overrides the period, 0 switches it off, and COMPOSER_POLICY_COOLDOWN_BLOCK overrides block.
One detail of the environment variable deserves attention from platform teams. A non-zero COMPOSER_POLICY_COOLDOWN_PERIOD enables the cooldown even when a project's composer.json sets cooldown to false. Only "policy": false switches it off again. A CI platform can therefore switch on a cooldown for every project without touching a single composer.json. A project can still opt out, with "block": false, broad ignore rules or --no-cooldown.
In July I guessed that this policy would arrive through the same mechanism as the malware filter list. I was half right. The cooldown shares the configuration skeleton of the lists, block, audit and ignore, but gets a pool filter of its own, because it judges age and has nothing to fetch. The name minimum-release-age stays on the list of reserved policy names.
The implementation started as pull request #12692 by Viktor Djupsjöbacka and landed on 1 October 2026, with Jordi Boggiano as co-author. Jordi then reworked the error messages, outdated and show --latest, and added the --no-cooldown flag on the day of the release candidate.
The clock matters more than the duration
A cooldown can only be as trustworthy as the timestamp it counts from. Composer packages have long carried a time field. The package author sets it, either explicitly in composer.json or implicitly through the date of the tagged commit, and Git accepts any commit date you give it. Someone who has taken over a repository can backdate a malicious release by a month, and a cooldown that trusts time waves it through.
Composer 2.11 counts from published-time instead: the moment Packagist.org first made the version available, a field Packagist.org added to its metadata for this purpose. Package authors have no influence on it. Composer insists on it, too. With a cooldown configured, a version from a Composer repository that lacks published-time stops the command, and the error names the package. For a repository that cannot provide the field, a Satis-generated one for instance, you can opt out with "require-published-time": false. Composer then falls back to time, and its documentation says plainly that the protection is weaker. Packages from VCS, path and artifact repositories always fall back, and Composer prints a warning when it judged versions by a date nobody vouched for.
How far the two clocks can drift apart shows in Composer's own composer.lock at the 2.11.0 tag. Composer 2.11 writes published-time into the lock file next to time, and for 21 of its 36 packages the two differ by less than an hour. The other 15 all come from Symfony. For composer/ca-bundle 1.6.0, time says 20:14 UTC on 20 October and published-time says 20:17. Then there is symfony/filesystem 5.4.54: time says 30 August 2026, published-time says 15 October 2026. For symfony/polyfill-php73 1.37.0, the gap exceeds a year and a half.
Nothing sinister happened there. Symfony tags all its split repositories with every release, including those whose code did not change, so the tag points at an old commit. But it shows what time measures: the age of a commit, not the age of a release. Measured by time, version 5.4.54 of symfony/filesystem would have cleared a seven-day cooldown 39 days before anyone could install it. Nesbitt describes the same flaw in Dependabot's cooldown for GitHub Actions, which counts from the tag's commit date.
A trusted clock alone does not suffice, though. published-time belongs to a version, and a version maps a number to a Git reference. If that reference could still move, a version would keep its old date while its code changed underneath. This explains why the feature had to wait for Packagist.org. In issue #12633, Nils Adermann described it as blocked by changes to the release metadata on Packagist.org, and his release post names published-time as the piece that kept the policy out of 2.10. Since 7 July, the reference of a stable version on Packagist.org can no longer change. Only together do immutable versions and published-time make the cooldown hold. Private Packagist, which also serves private and mirrored packages whose tags may still move, takes the idea one step further: according to the release post, the cooldown starts again when the code behind a version changes, after a force-pushed tag, for instance.
The cooldown guards the update, not the install
The cooldown applies when Composer chooses versions: composer update, require and remove. composer require and init also skip withheld versions when they write a constraint, and create-project does the same when it picks the project version, so composer require does not write ^2.0 into your composer.json when 2.0.0 appeared yesterday. As long as a lock file exists, composer install ignores the cooldown. It reproduces a decision the lock file already records.
Some versions never wait:
- Dev versions such as
dev-main, because they track a moving branch anyway - Locked packages that a partial update such as
composer update vendor/packageleaves alone - Platform packages: PHP, extensions and libraries
- Versions without any date, such as an inline
packagerepository entry withouttime; Composer cannot tell their age and lets them through rather than block them forever
The second exception has a flip side that will surprise some teams. A full composer update re-evaluates every package. If your lock file contains a version that is still inside the cooldown, perhaps because someone pulled it in with --no-cooldown, the next full update replaces it with an older release, or fails when no older release satisfies your constraints. Composer resolves strictly: when nothing old enough fits, the update stops. It does not quietly fall back to a version that is too new.
composer outdated and composer show --latest now tell the same story as composer update. They report the newest version the cooldown allows and show a newer release that is still waiting next to it, with the time left: “6d 4h”, for instance. When no installable update exists, the waiting release takes the place of the latest version, marked c, and the JSON output reports "latest-status": "cooldown". Waiting releases do not count as outdated for --strict, and --cooldown-period 0 shows the newest releases regardless. When the cooldown first landed on the main branch, outdated still recommended versions that update refused to install; pull request #13102 fixed that before any release shipped.
Two changes reach your lock file. Composer 2.11 records published-time for every package that comes from Packagist.org. And pull request #13149 makes config.policy part of the lock file's content hash, so changing the cooldown period or its exceptions flags the lock file as outdated. If your project already has a policy configuration, Composer reports the lock file as outdated after the upgrade until you refresh it, with composer update --lock, for instance.
Composer's own exceptions came within days
On 19 October, Jordi Boggiano added a seven-day cooldown to Composer's own composer.json. Two days later, Composer needed a release that was less than a day old. Pull request #13146 makes Composer reject TLS connections that present the compromised eMudhra intermediate certificates, even when the system's CA store still trusts their roots. The check relies on a denylist in composer/ca-bundle, and the same pull request updated that package to 1.6.0, which had reached Packagist.org the evening before.
The same evening, pull request #13149 recorded the exception. Composer's composer.json at the 2.11.0 tag reads:
"policy": {
"cooldown": {
"period": "7days",
"ignore": {
"composer/ca-bundle": {"constraint": "1.6.0", "reason": "Required and approved"}
}
}
}
The exception names one version, not the package. Version 1.6.1 will wait its seven days like any other release. The reason lives in the repository's history, where a reviewer can question it. That is the model Composer 2.11 offers for urgent fixes: an explicit decision, scoped to one version and written down.
The release itself shows the other path. Composer's lock file at the 2.11.0 tag contains composer/semver 3.6.0. Packagist.org made that version available at 09:14 UTC on 29 October. Six minutes later, it entered the lock file together with a resolver performance change, pull request #13174, and Composer 2.11.0 was tagged at 09:23. The release ships with a dependency that was nine minutes old. Version 3.5.2 had entered the lock file six days earlier, less than a day after its publication. Neither version appears under ignore. Whether by --no-cooldown, by the environment variable or with a Composer that does not know the policy yet, the configuration records nothing.
I do not read this as carelessness. composer/semver comes from the same team, and trusting your own packages is exactly what the ignore option exists for. But it shows the price of the one-off bypass: it leaves no trace. An ignore rule for the team's own packages would have recorded the same decision.
Other tools automate the exception for security fixes. According to Nesbitt, Hex lifts its cooldown for a package whose locked version has been retired or carries an advisory, and Dependabot skips its cooldown for security updates. Earlier iterations of pull request #12692 proposed something similar for Composer: a release published after a matching advisory, and not affected by it, would have skipped the cooldown. Composer 2.11.0 contains no such rule, and the release post says why: “Composer cannot safely let security fixes skip the cooldown policy automatically.” Whoever can publish a malicious release can usually publish an advisory, too, and “any rule that exempts releases based on advisory data would give attackers a way around the cooldown period.”
I would add a second reason, and it goes back to April. GitHub changed the affected versions of an advisory I had published for PHPUnit, GHSA-qrr6-mg7r-m243, without consulting me. Composer 2.9 then blocked all of PHPUnit 11, which never contained the vulnerability. I described the episode at the time. Advisory data can be wrong without anyone attacking it, and when it is, an automatic exemption lets the wrong version skip the queue.
The manual model has its price, too. It costs time precisely when time hurts most, and it depends on someone noticing that the fix they need sits in the waiting room. composer outdated at least shows that, with the time left.
Pull request #13146 shows a second trade-off. Composer's CI now sets COMPOSER_POLICY_COOLDOWN_PERIOD to 0 for the jobs that update dependencies, among them those that test the highest and the lowest dependency versions. Composer's documentation for library authors suggests a cooldown for precisely such jobs. Both positions hold up: a job that exists to catch breakage from new releases early wants those releases early. Such a job then deserves the treatment the same documentation prescribes: no secrets, no write permissions, and no composer update anywhere near a build or release step.
Strong defaults stop at the cooldown
Composer 2.9 blocks versions with known security advisories by default. Composer 2.10 blocks malware by default, even during composer install. Composer 2.11 waits for nobody unless asked. Without a period, the cooldown does nothing.
In July I wrote that opt-in protection reaches only those who know about it and switch it on. That still holds, and it applies here. The teams that read release notes and configure Renovate cooldowns will add a few lines to their composer.json. The project that someone set up three years ago and that runs composer update in a nightly job will not.
The question came up early. In issue #12633, Jordi Boggiano considered enabling the cooldown by default with a low value of six to twelve hours, and weighed the security gain against the disruption to existing workflows. pnpm made that call with version 11: since pnpm 11, minimumReleaseAge defaults to 1,440 minutes, one day. pnpm softened the change, though. Its built-in default resolves non-strictly and falls back to a version that is too new when nothing else fits.
As I read it, Composer's design would make a default harder. Its cooldown resolves strictly. A Composer repository without published-time stops the command. A default would therefore have broken updates against every Satis repository and every private registry that does not deliver publication times yet, on the day people upgraded Composer.
Organisations on Private Packagist have a default of sorts already. According to the release post, administrators can enforce a cooldown for every project that uses the organisation's Composer repository, whatever a project's composer.json says and whichever Composer version it runs. Private Packagist also provides publication times for mirrored and private packages, which removes the obstacle from the previous paragraph. The projects nobody looks after any more rarely come with a Private Packagist subscription, though.
I understand the decision, and I still hope it is not the last word. The release post leaves the question open. A default of a few hours, the kind discussed in #12633, would protect exactly those projects, and they need protection most.
Where I would start
The length of the wait sets one clock against another: the time it takes to find a malicious release, and the time a security fix spends in the waiting room. The release post recommends “a period of a few hours to a day”. Composer's own repository waits seven days, Dependabot three by default, Snyk 21.
For an application whose team watches advisories and knows --no-cooldown, I would start where Composer itself started: seven days. Without such a routine, the cooldown also decides how long a security fix waits, and a day is the better choice.
{
"config": {
"policy": {
"cooldown": {
"period": "7 days",
"audit": "report",
"ignore": {
"example/*": "Our own packages, released through our own pipeline"
}
}
}
}
}
Seven days matches the baseline Woodruff argues for, and zizmor flags Dependabot configurations that wait less. With "audit": "report", composer audit lists installed versions that are still inside the cooldown, which tells you when someone pushed a fresh release past it. The exception for your own namespace assumes that your own packages reach your registry through a pipeline you trust. If they do not, leave it out. If they do, the rule puts on record what Composer's own composer/semver updates left unrecorded.
A cooldown for Renovate or Dependabot does not replace this. It covers the bot's pull requests. It does not cover the developer who runs composer update on a Friday afternoon, nor the coding agent that runs it on their behalf. A cooldown in composer.json covers every update, wherever it runs. Developers can go one step further and set it for every project on their machine with composer config --global policy.cooldown.period "7 days", and platform teams can set COMPOSER_POLICY_COOLDOWN_PERIOD in their CI images.
The cooldown and the malware list work as a pair. The list can only block what someone has already flagged; the cooldown gives them the days to flag it. Composer 2.10 switched on the list for everyone. With 2.11, the other half takes a few lines of configuration.
Your own lists now reach production
The second change in 2.11 matters most to organisations with their own rules about dependencies. Composer 2.10 introduced custom dependency policies: named lists, served from an HTTPS endpoint or a Composer repository, that block package versions the same way the malware list does. Composer sends the Package URLs of the candidate packages to each source and gets back the versions to block. One gap remained. A custom list could block versions during composer update, require and remove, but never during composer install. Only the built-in malware list could do that.
Stephan Vock's pull request #12990 names the case that motivated the change. Policies change over time, so an existing lock file can suddenly contain packages the organisation no longer allows. Composer's documentation adds the other half: in CI and in deployment, often nothing but composer install runs, and an update-only block never fires there.
Composer 2.11 adds block-scope to custom policies, with the same three values the malware policy already knew:
-
updateblocks duringupdate,requireandremove, as before -
installblocks duringinstallonly -
allblocks in both cases
{
"config": {
"policy": {
"legal-hold": {
"sources": [
{"type": "url", "url": "https://policies.example.com/composer/legal-hold"}
],
"block-scope": "all"
}
}
}
}
The default for custom policies stays update, so nothing changes for existing configurations. Blocking during install has to be requested. The malware policy keeps its default of all.
I find the resulting division of labour coherent. composer update asks which version to take; the cooldown and the advisories answer that question, and their blocking ends there. composer install asks whether you may deploy what was chosen. Malware and, from 2.11 on, your own lists can answer that question, too.
Two consequences need thought before you switch block-scope to all. First, every environment that runs composer install now contacts the list's sources and sends them the names of your dependencies. Your build and deployment environments need network access to those sources, and the operator of a third-party list learns what you deploy.
Second, the lists fail open by default. The ignore-unreachable setting defaults to ["update", "install"]: when a source does not answer or returns anything but a 200, Composer skips it during updates and installs with nothing more than a warning. Only composer audit fails. If your list carries legal or security weight, set ignore-unreachable to false. The setting covers every repository and every policy source, though, including the malware list Packagist.org provides, not just your own list. An unreachable source then stops the deployment instead of waving everything through, and the availability of all these sources becomes part of your deployment's availability. Which of the two failures you prefer is a decision only you can make, but you should make it deliberately.
Conclusion
In July I ended on the question of how fast the PHP world could close its gaps. For the cooldown, the answer is not quite four months after its precondition went live. The design takes the hard part seriously. A cooldown that counts from time would have been easy to build and easy to fool. Composer counts from a timestamp only Packagist.org controls, refuses to guess when a Composer repository cannot provide it, and makes lasting exceptions explicit decisions that end up in version control. block-scope gives the lists organisations write for themselves the same reach the malware list already had.
Viktor Djupsjöbacka started the cooldown, Jordi Boggiano finished and refined it, and Stephan Vock built block-scope, with Nils Adermann and Christophe Coevoet reviewing along the way. Since 30 July, the Composer & Packagist Sponsorship Program gives companies a formal way to pay for this kind of work.
The default remains open. Composer's defences against known threats protect everyone, including the projects nobody looks after any more. Its defence against threats nobody knows about yet protects those who read release notes.
The most important sources for this article are Nils Adermann's release post for Composer 2.11 (29 October 2026), the changelog and the documentation at the 2.11.0 tag, pull requests #12692, #12990, #13102 and #13153, and Andrew Nesbitt's “Package Managers Need to Cool Down”. All other sources are linked where the text uses them.