Ask anyone who's run a Drupal major version upgrade what actually ate the calendar, and it's rarely core itself. It's almost always contributed modules — specifically, the handful you didn't know were a problem until you were already mid-upgrade. A proper audit up front turns that surprise into a known, budgeted task.
Why this matters more than it seems
Upgrading Drupal core is, at this point, a fairly mechanical process (see the companion post on the 10-to-11 upgrade). What isn't mechanical is figuring out, module by module, whether the code your site depends on will even run on the target version. Do this audit before you touch core, not during — Upgrade Status can't meaningfully check most compatibility issues once you've already upgraded, because the deprecated APIs it's checking against no longer exist to compare.
Step 1: Get the full list
Start with a straightforward inventory of every enabled module:
drush pml --status=enabled --no-core
Cross-reference this against what's actually used — it's common for sites to have modules enabled that were installed for a feature nobody uses anymore. Every module you can honestly uninstall before the upgrade is one you don't have to audit.
Step 2: Let Upgrade Status do the sorting
composer require drupal/upgrade_status drush en upgrade_status -y
Run a scan from Administration > Reports > Upgrade Status, or via Drush:
drush upgrade_status:analyze --all --ignore-uninstalled
Every contributed module you have will land in roughly one of three states:
- Has a stable release for the target version — update it, no further thought required.
- Has an issue queue conversation but no stable release — worth investigating; a patch might already exist.
- No compatibility work in progress at all — this is where the real audit work happens.
A typical mid-sized site with 30-40 contributed modules usually has one to three modules in that third category. Finding them during the audit, rather than mid-upgrade, is the entire point of doing this early.
Step 3: Investigate the "state 3" modules individually
For each module with no compatibility path yet, check a few things on its drupal.org project page:
- Maintenance status. Is it marked unsupported or abandoned? Check the issue queue for recent activity — a module with commits in the last few months behaves very differently from one that's been silent for two years.
- Usage statistics. A module used by a handful of sites is far less likely to get community-driven compatibility work than one used by thousands.
- Existing patches. Search the issue queue for the target Drupal version number — someone has often already done partial work, even if it hasn't landed as a stable release.
- The
core_version_requirementin its.info.yml. Sometimes a module already works fine on the new major version but simply hasn't updated its declared compatibility — a quick local test, or widening the constraint yourself, tells you if that's the case.
Step 4: Pick a strategy per module
For each module still without a clear path, you have four realistic options, cheapest first:
- Apply an existing patch from the issue queue, even an unofficial one, and pin it via
cweagans/composer-patches. - Replace it with a maintained equivalent. Sometimes another module does 90% of the same job and is actively supported.
- Port it yourself. If the module is small and important to your site, forking it and doing the deprecation cleanup — with Rector's help — is often less work than it sounds.
- Drop the feature. If nobody's used the functionality in a year, this is the cheapest option of all, and audits are exactly when you find that out.
Step 5: Check what moved out of core entirely
Every major version quietly moves a few things from core into contrib, or removes them outright. Going from Drupal 9 to 10, for example, modules like Color, RDF, and Aggregator were deprecated and removed — sites using them need the contributed equivalent. Going from 10 to 11, more modules followed the same path. The Deprecated and obsolete extensions page on Drupal.org is the canonical list — check it for every version you're jumping across, not just the most recent one.
Step 6: Watch for custom patches
If your composer.json applies patches to any contributed module via composer-patches, each of those patches is version-specific — it was written against a particular release and may not apply cleanly (or at all) once the module updates. Audit your patch list alongside your module list, and check whether each patch has since been fixed upstream and can simply be dropped.
What "done" looks like
The audit is complete when you have a written list of every module in states 2 and 3, each with an assigned strategy and a rough time estimate. If you still have several "I don't know yet" items, you're not ready to schedule the actual upgrade — that uncertainty is exactly what turns a planned two-week project into an unplanned six-week one. The upgrade command itself, once dependencies are sorted, runs in hours. It's this audit that determines whether your calendar estimate is honest.