11 min read

Composer 2.11's Cooldown in a Laravel App: Safer Updates, Except When the Update Is the Security Fix

I turned on the new Composer cooldown in a Laravel 13 project. The first full update failed, because a security fix was in cooldown.

Composer 2.11's Cooldown in a Laravel App: Safer Updates, Except When the Update Is the Security Fix

On 31 October I turned on Composer 2.11's new cooldown in a Laravel 13 project, set it to seven days, and ran composer update. It failed. Three days earlier Symfony had shipped a fix for CVE-2026-45070 in symfony/mime. The cooldown was holding the fix back, and Composer's advisory policy was blocking every older version. My project had nothing it was allowed to install.

The release notes do say the cooldown applies to security fixes too. What they can't show is how fast that bites. The cooldown gives malware scanners time to flag a malicious version before composer update ever offers it to you. But the same rule applies to the version that fixes your vulnerability.

I tried it on a clean Laravel project with Composer 2.11.1, with laravel/framework locked at 13.37.0. Here's what it changed, where it broke, and the setup I'd use. All output below is real.

What the cooldown does

Composer 2.11.0 shipped on 29 October, and 2.11.1 followed a day later. The cooldown is part of the policy config. It's off by default, and it takes effect once you set a period:

composer config policy.cooldown.period "7 days"
"config": {
    "policy": {
        "cooldown": {
            "period": "7 days"
        }
    }
}

The period takes values like "6 hours", "1 day" or "1 week", measured from the publish time the repository reports for each version. It applies when Composer resolves dependencies, so update, require and create-project. composer install still installs exactly what's in composer.lock. Dev versions like dev-main are never withheld.

What it did to a Laravel 13 project

13.39.0 came out on 27 October, four days before I ran this. With a seven-day period, updating the framework stopped one release short:

$ composer update laravel/framework
Loading composer repositories with package information
31 package version(s) withheld by the cooldown policy (run with -vv to list them).
Updating dependencies
Lock file operations: 0 installs, 1 update, 0 removals
  - Upgrading laravel/framework (v13.37.0 => v13.38.0)

31 versions for one package, because the list also counts the illuminate/* packages Laravel publishes alongside each release, a few seconds apart. -vv lists them:

31 package version(s) withheld by the cooldown policy:
  - laravel/framework (v13.39.0) published 2026-10-27T14:05:37+00:00, available in 3 days 4 hours
  - illuminate/bus (v13.39.0) published 2026-10-27T14:04:59+00:00, available in 3 days 4 hours
  - illuminate/bus (v12.70.2) published 2026-10-27T14:08:11+00:00, available in 3 days 4 hours
  - illuminate/collections (v13.39.0) published 2026-10-27T14:05:01+00:00, available in 3 days 4 hours

composer outdated has a new marker for it:

$ composer outdated --direct
Legend:
! patch or minor release available - update recommended
~ major release available - update possible
c newer release still in the cooldown period - not installable yet
laravel/framework 13.38.0 c 13.39.0 (3d 4h left) The Laravel Framework.
phpunit/phpunit   12.5.42 c 13.4.4 (2d 17h left) The PHP Unit Testing framew...

And --no-cooldown skips it for one package, one command:

$ composer update --no-cooldown laravel/framework
  - Upgrading laravel/framework (v13.38.0 => v13.39.0)

So far it does what it says. The surprises started with a full update.

A full update downgraded 23 packages

With laravel/framework now at 13.39.0, still inside the cooldown, I ran a plain composer update. I'll get to why it failed in the next section. With the package behind that failure exempted, here is what the same update planned:

$ composer update --dry-run
78 package version(s) withheld by the cooldown policy (run with -vv to list them).
Updating dependencies
Lock file operations: 0 installs, 23 updates, 0 removals
  - Downgrading doctrine/inflector (2.1.1 => 2.1.0)
  - Downgrading guzzlehttp/guzzle (8.3.1 => 8.3.0)
  - Downgrading laravel/framework (v13.39.0 => v13.38.0)
  - Downgrading league/commonmark (2.10.6 => 2.10.4)
  - Downgrading nesbot/carbon (3.15.1 => 3.15.0)
  - Downgrading symfony/console (v7.4.22 => v7.4.21)
  - Downgrading symfony/http-kernel (v7.4.22 => v7.4.21)
  - Downgrading symfony/mailer (v7.4.22 => v7.4.19)
  ...

The Packagist announcement warns about this. A full composer update re-checks every package, and anything locked at a version still inside the window gets swapped for an older one. A partial update leaves unrelated packages alone. If you add a cooldown to a project that's already up to date, your first full update moves it backwards. Run it with --dry-run first and read the list.

When the update is the security fix

CVE-2026-45070 is an email header injection in symfony/mime, rated medium. The advisory was published on 28 October, alongside the fixed releases, 7.4.22 and 8.1.10. laravel/framework requires symfony/mime, so every Laravel app has it.

Composer also blocks versions with known advisories during update by default (policy.advisories.block defaults to true). Put the two policies together and a project still on the vulnerable 7.4.19 has nowhere to go:

$ composer update symfony/mime
2 package version(s) withheld by the cooldown policy (run with -vv to list them).
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
  Problem 1
    - laravel/framework v13.39.0 requires symfony/mime ^7.4.0 || ^8.0.0 -> found symfony/mime[v7.4.0, ..., v7.4.22, v8.0.0, ..., v8.1.10] but these were not loaded, because:
      - v7.4.12, v7.4.13, v7.4.15, v7.4.16, v7.4.17, v7.4.18, v7.4.19, v8.0.12, v8.0.13, v8.0.15, v8.1.0, v8.1.2, v8.1.4, v8.1.5, v8.1.6, v8.1.7: affected by security advisories ("PKSA-wtxr-p26d-nn42")
      - v7.4.22, v8.1.10: still in the cooldown period configured in "policy.cooldown" (available in 4 days 9 hours)

Composer refuses to move you to a version it knows is vulnerable, and it refuses to move you to the fix because it's too new. So you stay on the version you're on, which is also vulnerable. And nothing at deploy time tells you. composer install installed 7.4.19 from the lock file without a word and exited 0.

composer audit is the one that speaks up. It found the advisory and exited 1:

$ composer audit
Found 1 security vulnerability advisory affecting 1 package:
+-------------------+----------------------------------------------------------------------------------+
| Package           | symfony/mime                                                                     |
| Severity          | medium                                                                           |
| Advisory ID       | PKSA-wtxr-p26d-nn42                                                              |
| CVE               | CVE-2026-45070                                                                   |
| Title             | CVE-2026-45070: Email Header Injection via Non-Token Characters in Mime          |
|                   | Parameter Names                                                                  |
...

The way out is one targeted command:

$ composer update symfony/mime --no-cooldown
  - Upgrading symfony/mime (v7.4.19 => v7.4.22)
No security vulnerability advisories found.

Use --no-cooldown here, not --no-blocking. Composer 2.11 has both, and the help text explains the difference. --no-blocking "disables all policy blocking during this command", which includes the advisory and malware checks you want to keep. --no-cooldown turns off the cooldown only, and the other policies still apply.

What a Composer cooldown window does A malicious release is held back for the cooldown, so a scanner can flag it before your update ever offers it. A security fix is held back the same way, and because the advisory blocks every older version, composer update fails until the fix leaves a 7-day cooldown, while a 1-day period would allow it the next day. Malicious release any package 1-day cooldown Security fix symfony/mime 7.4.22 7-day cooldown published your composer update: first day it could be offered flagged by a scanner inside the window, it never reaches your lock file fix and advisory published, 28 Oct update fails: the fix is withheld, every older version is blocked by the advisory a 1-day period would allow it here 31 Oct: my full update fails 4 Nov: fix offered 0d 1d 2d 3d 4d 5d 6d 7d days after publish LEGEND version published cooldown window cooldown window where updates fail

New projects fail the same way. With a seven-day period set through the environment, a fresh install broke that morning:

$ COMPOSER_POLICY_COOLDOWN_PERIOD="7 days" composer create-project laravel/laravel fresh-under-cooldown
Installing laravel/laravel (v13.11.0)
81 package version(s) withheld by the cooldown policy (run with -vv to list them).
Your requirements could not be resolved to an installable set of packages.
      - v7.4.22: still in the cooldown period configured in "policy.cooldown" (available in 4 days 8 hours)

laravel new runs composer create-project under the hood, so a cooldown in your global Composer config affects it too.

Your own packages

Two kinds of repository came up in my test, and they behaved differently.

A path repository, the usual setup for a package that lives in your monorepo, installed fine. Composer warned that it couldn't verify the publish date and moved on:

The publication date of 1 package version(s) from path repo (packages/acme-helpers) could not be verified, the cooldown policy relied on the time field set by the package authors (run with -vv to list them).
  - Installing acme/helpers (1.0.0): Symlinking from packages/acme-helpers

A git repository with a tag I'd created a minute earlier did not:

Problem 1
  - Root composer.json requires acme/sdk ^1.0, found acme/sdk[v1.0.0] but these were not loaded, because they are still in the cooldown period configured in "policy.cooldown" (available in 6 days 23 hours, based on the package-supplied time field as the repository provides no published-time).

That's the in-house SDK your team tagged this morning, blocked for a week. The ignore list fixes it, with a reason that ends up in composer.json for the next person:

"cooldown": {
    "period": "7 days",
    "ignore": {
        "acme/*": "Our own packages, tagged by us"
    }
}

With that in place, the same composer require acme/sdk locked v1.0.0 straight away. If you serve private packages from your own Composer repository, check what it reports. The Composer docs say a repository that doesn't provide published-time makes the command error out while a cooldown is set, and a trusted repository can opt back into the older time field with require-published-time set to false.

Which period to pick

The Packagist announcement suggests a few hours to a day. I ran the same full-update dry run with four periods, at 09:21 UTC on 31 October:

Period Versions withheld Full composer update
1 hour 0 Resolves
1 day 2 Resolves (one bugfix release of league/commonmark rolled back)
3 days 41 Fails on symfony/mime
7 days 81 Fails on symfony/mime

A day matches Packagist's own suggestion, and a security fix stops blocking you by the next working day. With three days, my full update stayed broken until the evening of 31 October. With seven, it stays broken until 4 November, all because one package every Laravel app depends on shipped a security fix.

In CI you don't have to commit the setting. COMPOSER_POLICY_COOLDOWN_PERIOD sets it from the environment. With nothing in composer.json, this was enough for outdated to apply it:

$ COMPOSER_POLICY_COOLDOWN_PERIOD="7 days" composer outdated --direct
Cannot use laravel/framework's latest version v13.39.0 as it is still in the cooldown period configured in "policy.cooldown" (available in 3 days 4 hours).

The setup I'd use

A one-day period, with your own vendor name exempt:

"config": {
    "policy": {
        "cooldown": {
            "period": "1 day",
            "ignore": {
                "acme/*": "Our own packages, tagged by us"
            }
        }
    }
}

Add composer audit to CI as its own step, because composer install won't tell you about a vulnerable lock file and audit exits 1 when it finds one. If you don't have a pipeline yet, my GitHub Actions guide has a starting point. And write the escape hatch down before you need it. When audit fails and the fix is still in cooldown, run composer update vendor/package --no-cooldown for that one package.

The cooldown and the audit cover different risks. The cooldown keeps a brand new malicious version out of your lock file. The audit catches a known vulnerability already in it. I wrote about the first problem after fake Laravel packages started targeting .env files. A cooldown only helps if the fake is caught inside the window, so it doesn't replace reading what you install.

If you're mid-upgrade, the cooldown adds one more kind of line to your conflict output. The usual Laravel 13 upgrade conflicts still come first, and a release you're waiting for may now also be "still in the cooldown period".

FAQ

Does the Composer cooldown affect composer install?

No. composer install installs exactly what's in composer.lock, whatever the cooldown says. The cooldown applies when Composer resolves versions, during update, require and create-project.

Does the cooldown hold back security fixes?

Yes. Composer doesn't exempt them. The Packagist announcement explains why. Any rule that exempted releases based on advisory data would give attackers a way around the cooldown. Run composer update vendor/package --no-cooldown when you need a fix sooner.

What's the difference between --no-cooldown and --no-blocking?

--no-cooldown turns off the cooldown for one command and keeps the advisory and malware policies active. --no-blocking turns off all policy blocking, including those. For a security fix, --no-cooldown is the one you want.

Why does composer update downgrade packages after I enable a cooldown?

A full composer update re-evaluates every package against the cooldown. A version you already have locked that is still inside the window gets replaced with the newest version outside it. A partial update, like composer update vendor/package, leaves other packages alone.

Do my production servers need Composer 2.11?

Not for the cooldown. It only matters where you run update or require, which is usually your machine or CI. Deploys that run composer install from the lock file aren't affected. Upgrade anyway, though, because 2.11 also ships security fixes of its own, listed in the release notes.

Conclusion

Set the cooldown to a day, exempt your own packages, and run composer audit in CI. Then keep composer update vendor/package --no-cooldown somewhere your team will find it. In the week I tried it, the cooldown's first real cost was a security fix, and only the audit told me it mattered.

Share: X/Twitter | LinkedIn | | RSS
Hafiz Riaz

About Hafiz

Senior Full Stack Developer. I build production software with Laravel, Filament, Vue, and AI integrations, and write about the real decisions behind shipping it.

Get in touch →

Get web development tips via email

Join 50+ developers • No spam • Unsubscribe anytime