Why our Drupal sites now wait a day for updates

Main Image
11. November 2026

Why our Drupal sites now wait a day for updates

by Jürgen Haas

On 10 November, consolidation/robo released version 5.2.0. According to its time field, it was three years old.

The git tag pointed at a 2022 commit, and time said 7 December 2022. The package author controls that field and can backdate it to whatever they like. Packagist's own published-time said 10 November 2026, 19:16:15 UTC. Harmless this time. It still broke the outdated packages check in 15 of our projects. I reported it as consolidation/robo#1196.

Composer 2.11's new cooldown ignores time. It counts from published-time, which Packagist sets itself when it first sees a version.

Since 11 November, our Drupal projects wait one day before they take a new release. Fixes for Drupal security advisories can jump the queue. Getting there took a policy file, a plugin release, three additions Composer doesn't ship, and a spell checker that had never heard of a cooldown.

Waiting is the feature

Composer 2.11 withholds new releases for a configurable period. The Composer team recommends "a few hours to a day". The cooldown detects nothing. It buys time for somebody to notice a bad release before our CI installs it.

packagist.org also runs a malware feed, announced as filter.lists.malware in its packages.json. The packages.json of packages.drupal.org has no filter key, so Composer has no feed to use there. For drupal/* packages, the cooldown is the only delay Composer can apply.

Can we trust the timestamps? Yes. drupal.org sets published-time for every version itself. In three samples (eca 3.1.11, token 1.18.0, gin 5.0.16), it matched the creation time of the release node within about 20 seconds.

One file, no project touched

The policy is one file: cooldown.json in composer/drupal-default-config. Shortened, it reads:

{
  "period": "1 day",
  "audit": "report",
  "ignore": {
    "lakedrops/*": "LakeDrops packages",
    "bitegra/*": "LakeDrops private packages; our GitLab registry provides no publication time",
    "drupal-eca-recipe/*": "LakeDrops ECA recipes"
  }
}

Distribution was the easy part. lakedrops/drupal-config is already in every LakeDrops project, as a dependency of lakedrops/drupal-environment, and every CI job installs it globally. With extra.lakedrops-default: true, it already pulls our central Composer settings. Since 1.0.17, it also reads cooldown.json and merges it into Composer's in-memory config on Composer 2.11 and later. Whatever a project or a developer sets wins. Fill, don't override.

In the lab, composer.json and composer.lock stayed byte-identical.

Three places it couldn't go

composer.json. In Composer 2.11, policy is part of the lock file's content hash. In the lab, I added it to a project's composer.json and composer validate called the lock outdated, exit code 2. The same policy in the global config didn't. Going that route meant editing and relocking every project.

The Docker image. L3D, our local environment, mounts the developer's home directory. Composer reads its global config only from there. Whatever we bake into the image never gets read locally.

An environment variable. The cooldown variables set the period or the block switch, not the ignore list. COMPOSER_AUTH only accepts auth settings, since Composer 2.11.2. And without the ignore list, projects on our private GitLab Composer registries fail outright. Those registries provide neither published-time nor time, and Composer refuses to guess:

... has no published-time, so the cooldown policy ... cannot tell whether it is new or old

Who waits, and how long

Our own namespaces skip the cooldown. We control them, and our private registries carry no timestamps.

drupal/* waits. The project lock files we checked hold between 65 and 179 drupal/* packages each, from many different maintainers. There's no malware feed for them, so the cooldown is all the delay there is.

One day is the upper end of the recommendation, on purpose. Our automated updates get committed and released without a human review. And with a nightly schedule, the effective delay is 24 to 48 hours anyway.

Longer hurts. With one week in the lab, the update became unresolvable. The releases fixing symfony/filesystem (advisory PKSA-g499-cm4q-v35y) were still in the cooldown, while advisory blocking rejected the older ones. Too young or too dangerous, pick one.

A project that needs a different period sets Composer's own COMPOSER_POLICY_COOLDOWN_PERIOD, or puts it in its config.

Three things Composer doesn't do for us

The policy is Composer's. The next three are ours: LakeDrops additions that run in our CI.

Lock trust

A full composer update re-evaluates every package, locked or not. In the lab, a one-week cooldown downgraded locked versions that were still inside the window: composer/composer from 2.11.2 to 2.11.1, plus several symfony packages.

It gets better. With advisory blocking off, which is how our CI runs, symfony/filesystem went from v8.1.11 back to v8.1.9. That version is affected by PKSA-g499-cm4q-v35y. A supply chain protection, downgrading us into a known vulnerability. Well done, everybody.

With advisory blocking on, it went to 8.2.x-dev instead. All our projects use minimum-stability: dev, and dev versions are never withheld.

So before the update job and the outdated check run, the plugin command lakedrops:cooldown:exemptions exempts every locked version still inside the cooldown, by exact version. The same lab update then reported "Nothing to modify in lock file".

The stable-to-dev guard

The nightly update job fails if the cooldown would move a package from a stable release to a dev branch. Nothing gets committed. A failed job is annoying. A dev branch in an automated release is worse.

The security fast lane

The Composer team deliberately doesn't let security fixes skip the cooldown automatically. An attacker with access to a package's repository can publish an advisory themselves. Fair point.

Drupal advisories are different. SA-CORE-* and SA-CONTRIB-* come from the Drupal Security Team, not from the package maintainer. A compromised maintainer account can't create one.

So when a Drupal advisory affects a locked version, our update job exempts the releases that fix it. Only those published before the advisory was reported. A release published afterwards waits the full day, so nobody can ride on a genuine advisory. Vulnerable versions are never exempted. Our CI runs with advisory blocking off, so that rule has to hold.

This is our call, not Composer's. For general advisories, the Composer team's position is the opposite.

My lab test was SA-CONTRIB-2026-312 for Mime Mail, reported on 4 November at 17:51:58 UTC. drupal/mimemail 2.0.4 had been published at 15:48:17 UTC, two hours earlier. The fast lane exempted exactly ==2.0.4, and the update moved from 2.0.1 to 2.0.4 despite the cooldown.

What does that mean in hours? SA-CONTRIB-2026-175 for Webform, Critical, remote code execution, was reported on 23 September at 16:27:38 UTC. webform 6.3.1 came out at 14:56 UTC the same day. No cooldown existed back then, but the timestamps are real. With one day of cooldown and a nightly update at 01:00, a project gets 6.3.1 about 32 hours after the advisory. With the fast lane, about 8.5 hours.

One night, one update job, one day of patience

First run in production

The right rollout order is policy file, then plugin, then CI. Ours was CI first, calling a command from a plugin release that wasn't out yet.

Not by choice. The plugin's develop pipeline failed its spell check. On the word "cooldown". One scheduled update run failed until the release was out. A spell checker, holding up a supply chain feature. I didn't have that on my risk list.

On 11 November, our demo project ran its first scheduled update with everything live. The plugin loaded the central policy, and five brand-new package versions were withheld. Lock trust kept drush/drush 13.8.1, published the evening before, and doctrine/deprecations 1.2.0 instead of downgrading them. Our own lakedrops/drupal-config 1.0.17 went straight in, because its namespace is exempt.

Composer 2.10 simply ignores the injected policy. No older environment broke.

The outdated check did.

The release notes are right, except...

The Composer announcement says composer outdated points out releases still in their cooldown period. And it does. In the lab, an older installed version with a newer release in the cooldown gets a c marker and doesn't fail --strict. Exit code 0:

doctrine/deprecations 1.1.6 c 1.2.0 (21h left)

On day one, our demo project's check failed anyway. drush 13.8.1 and doctrine/deprecations 1.2.0 had been locked hours before the cooldown went live. So the installed versions themselves were inside the window.

Our check runs composer outdated --minor-only --strict, and the same with --patch-only. For drush, that searches ^13.8.1. The only stable version in that range is the installed one, and the cooldown held it back. With minimum-stability: dev, Composer fell back to the dev branch, which the cooldown never withholds, and called it an update:

drush/drush 13.8.1 ! 13.x-dev

Without --minor-only, the same command correctly said "Everything up to date". Composer 2.11.2 even has a guard for an installed version inside the cooldown. It only catches a fallback to an older release, not to a dev branch.

Waiting a day was no fix. The problem would come back after every fast-lane or --no-cooldown update. So the CI now applies the same lock trust exemptions before the outdated check. The next run passed, and all four outdated checks reported "Everything up to date".

I reported it upstream as composer/composer#13263, still open at the time of writing. It's related to the open composer/composer#13247: the same dev-branch fallback during update, which our stable-to-dev guard catches.

The fix surfaced one more detail. Composer loads a project's own copy of a plugin before a global copy of the same plugin. Projects still on an older lakedrops/drupal-config don't have the new command, so the CI step first checks that it exists. Those projects have no cooldown yet anyway.

What developers do

Drupal advisory: nothing. The next scheduled update takes the fix and releases it. Want it faster? Start the project's update schedule once after the advisory is published.

Any package, locally: composer update --no-cooldown vendor/package. That skips the cooldown only. Advisory and malware blocking stay active.

Don't disable the cooldown for a whole CI update to get one fix in. That lets every brand-new release through, not just the one you wanted.

Where it doesn't reach

There's no cooldown on the very first dependency resolution of a brand-new project, because the plugin isn't installed yet. None with --no-plugins either, and none in projects without lakedrops/drupal-config. Lock trust and the fast lane only run in the CI update and outdated jobs, not on a local composer update.

The fast lane knows Drupal advisories only. Fixes announced under GitHub or Packagist IDs, for Symfony, Guzzle or Twig, wait up to a day unless somebody updates manually. Follow-up releases published after an advisory wait the full day, too.

Two things haven't met real data yet. I tested the fast lane with a contrib advisory only. Core should work the same way, with the advisory from drupal.org and the release dates from Packagist. And the rule that never exempts releases published after an advisory is implemented, but no such release existed to try it on.

All of it is public: the policy, the plugin and the CI templates, whose AGENTS.md has a section "Security fixes and the cooldown". The cooldown also changes what our hourly reference site updates pick up. The other half of that safety net is weekly module testing.

Know an advisory source for Symfony and friends that a compromised maintainer account can't write to? Tell me in the comments or through our contact form.

Neuen Kommentar hinzufügen

Klartext

  • Keine HTML-Tags erlaubt.
  • Zeilenumbrüche und Absätze werden automatisch erzeugt.
  • Website- und E-Mail-Adressen werden automatisch in Links umgewandelt.
CAPTCHA
Diese Sicherheitsfrage überprüft, ob Sie ein menschlicher Besucher sind und verhindert automatisches Spamming.