Why migrate now (and why people keep delaying)
Drupal 9 went end-of-life on 1 November 2023. That date has been on the wall for two and a half years. Every Drupal 9 site running in production today is running unpatched — or running patches you're maintaining yourself by hand. Neither is OK on anything you depend on.
The pattern we see most often: the team did the maths, decided the upgrade was "too risky right now," and shipped a deferral that quietly became permanent. The fix is to stop treating this like a rebuild and treat it like a routine major version. Done right, Drupal 9 → 10 is the smallest jump in Drupal's recent history — most contrib modules already support it, and the deprecated API surface is small.
The migration isn't a rebuild. It's a cleanup. The teams that get into trouble are the teams who try to do everything else at the same time.
Step 1 — Run Upgrade Status, honestly
Install the Upgrade Status module on your Drupal 9 site and let it scan your codebase. The report will show three things you care about:
- Which contributed modules are Drupal 10 ready (most are by now)
- Which custom modules use deprecated APIs
- Which themes need work
Treat this report as the source of truth for the project. Every red row turns into a ticket. Don't skip rows because they "look minor" — they nearly always come back to bite during testing.
composer require drupal/upgrade_status --dev
drush en upgrade_status
# then visit /admin/reports/upgrade-status
Step 2 — Bring your dependencies current
Before touching core, bring every contributed module up to its latest Drupal 9–compatible release. This is non-negotiable. Trying to jump core when half your contrib is two minor versions behind is the single biggest source of avoidable migration pain.
Then make sure your local PHP version is at least 8.1 (Drupal 10's minimum). If you're still on PHP 7.4 or 8.0, fix that first — the order matters because some contrib modules require 8.1 even on Drupal 9.
Watch out for these dependencies
- jQuery UI — removed from core. If anything you wrote relies on it, you'll need the contributed
jquery_uimodule. - CKEditor 4 — also gone. Drupal 10 uses CKEditor 5 by default. Your custom plugins will need to be rewritten — this is the surprise cost on most migrations.
- Color module — gone. If your theme uses it for runtime colour overrides, switch to CSS custom properties.
- Quick Edit — gone. The
quickeditcontrib module covers it if you need it.
Step 3 — Fix the deprecated code
For every custom module flagged by Upgrade Status, fix the deprecated calls. The two tools that pay for themselves immediately:
- Drupal Rector — automated codemods that fix the bulk of common deprecations
- PHPStan with phpstan-drupal — catches deprecations Rector misses and stops them coming back
composer require palantirnet/drupal-rector --dev
vendor/bin/rector process web/modules/custom
vendor/bin/phpstan analyse web/modules/custom
Rector handles 70–80% of the work on a typical custom-code estate. The remaining 20% needs human eyes — usually around dependency injection patterns, plugin definitions, and entity API edge cases.
Step 4 — Run the core update
Once Upgrade Status is green or as close as you can get it, the core update itself is a Composer command:
composer require 'drupal/core-recommended:^10' 'drupal/core-composer-scaffold:^10' 'drupal/core-project-message:^10' --update-with-all-dependencies
drush updb
drush cr
Run this in a fresh feature branch, not directly on the develop branch. You'll iterate at least a few times before everything is clean.
Step 5 — Rebuild the theme (the part most teams underestimate)
This is where most projects blow past their estimate. Classy and Stable have been removed from Drupal 10 core. If your theme inherits from either, you need to either:
- Pull in
classyorstablefrom the contributed equivalents (the easy path) - Rebuild on Olivero or Claro (the long-term-correct path)
For sites with serious investment in the theme, we recommend option 1 for the migration window and then planning a separate theme refresh on Olivero with Single Directory Components afterwards. Trying to do both at once is how 6-week migrations become 6-month projects.
What to validate after the theme update
- CKEditor 5 styles render correctly in editor and on the public page
- Form layouts haven't broken (Claro is the new admin default — visual differences are real)
- Custom Twig templates still resolve — some template inheritance paths changed
- RTL languages still display correctly
Step 6 — Test under real load
Spin up a staging environment that mirrors production, restore a current production database, deploy the Drupal 10 build, and run:
- Visual regression — BackstopJS or Percy against a list of representative URLs
- Functional tests — Behat or PHPUnit on the critical user journeys (login, search, checkout, content submission)
- Performance baseline — measure LCP, INP, CLS, TTFB before and after
- Load test — k6 or JMeter against staging, scaled to peak production traffic
The number of times we've found a single misbehaving block or view that only shows up under concurrent load is too many to count. Don't skip this step.
Step 7 — Ship it on a Friday night (here's why)
For an enterprise Drupal estate, Friday evening through Sunday is the right deploy window for one reason: if something subtle breaks and content editors need to refresh their mental model of the admin UI, you have the weekend to fix it before Monday morning. Counter-intuitive but consistently right.
Production deploy checklist
- Take a verified database backup (and test the restore in staging)
- Put the site into maintenance mode
- Deploy the new build, run
drush updb -y,drush cim -y,drush cr - Smoke test the critical paths
- Take the site out of maintenance mode
- Monitor error rate and response time for 24 hours minimum
Key takeaways
- Drupal 9 → 10 is the smallest recent major upgrade. Treat it as a cleanup, not a rebuild.
- The two surprise costs are CKEditor 5 plugin migration and theme work where Classy / Stable were inherited.
- Rector + PHPStan covers 70–80% of the deprecated-code remediation automatically.
- Real-load testing catches the bugs that bite in production. Don't skip it.
- Plan the theme refresh as a separate project after the migration is in production.
If you'd rather not run this migration yourself, that's exactly the kind of thing we do every week. Get in touch — we'll come back with a clear scope and a realistic timeline within 48 hours.