Magento 2.4.9 Upgrade Guide
I upgraded my own Magento store from 2.4.8-p3 to 2.4.9 on 2026-05-13. Hit five distinct problems, fixed them all, and shipped clean. Here’s the exact playbook — every command, every trap, every workaround — so you can do the same upgrade without the surprises.
Is this upgrade safe for you?
Four common situations, four honest verdicts. Find the card that matches your store.
-
Coming from 2.4.8-p1 / p2 / p3
A 1–2 hour upgrade for a clean store. 1–2 weeks if you carry a long tail of 3rd-party modules. Most stores can ship this weekend — book a 30-minute maintenance window, take the backups, follow the 7 steps below.
-
Coming from 2.4.6 or 2.4.7
4–8 weeks. You jump multiple PHP versions and three infra changes in one go (Redis → Valkey, OpenSearch 2 → 3, Symfony 6 → 7.4). Don’t rush. Audit every paid extension first, then plan a hosting migration window, then run the Magento upgrade.
-
Hyvä Themes or Adobe Commerce B2B
Hyvä works on 2.4.9 — re-test your Hyvä Checkout adapter for the Braintree changes (Google Pay vaulting, Apple Pay on Chrome and Firefox). Adobe Commerce B2B needs a full smoke test of Companies, quotes, requisition lists, and shared catalogues.
-
Mid-peak-season trading
Don’t. Defer until after BFCM, Diwali, or your Q4. Adobe ships security patches for 2.4.8 until July 2027 — you can wait safely. The 5 minutes you save in the calendar are not worth a checkout outage on Black Friday.
The 5 traps you WILL hit
Universal — not vendor-specific. These five problems show up on every 2.4.9 upgrade I have done. Plain English next to every technical term.
-
01
composer requireerrors out on the metapackageAdobe’s
magento/product-community-editionis a metapackage. Since Composer 2.1.6,composer require magento/product-community-edition=2.4.9rejects it with “Use require-commerce instead.” The fix: Adobe shipsmagento/composer-root-update-plugin(bundled with 2.4.4+). Usecomposer require-commerceinstead. Plain English: the regular composer command can’t handle Magento’s root-package update logic — Adobe gives you a custom command for it. -
02
PHP 8.4 deprecates implicit-nullable parameters — and Magento turns the deprecation into a fatal
Any 3rd-party module written before PHP 8.4 that uses
Type $param = null(without the?prefix) emits “Deprecated Functionality: Implicitly marking parameter $X as nullable is deprecated.” Magento’s framework error handler upgrades the deprecation into a thrown exception in developer mode — so everybin/magentocommand crashes. Plain English: PHP 8.4 wants?Type $param = null(explicit nullable). The oldType $param = nullstyle is a syntax error in disguise. Detection: grep thevendor/tree for the pattern, excludingvendor/magento/(Adobe ships clean code). -
03
Symfony 7.4 enforces
: intreturn onCommand::execute()— old custom commands break at autoloadMagento 2.4.9 ships Symfony 7.4 LTS (up from Symfony 6 in 2.4.8). Symfony 7’s parent
Command::execute()is strict-typed with: int. Any custom CLI command (bin/magento your:command) that declaredexecute()with no return type or: voidfails LSP at class declaration. The fatal happens BEFORE Magento checks if the module is enabled — so disabling inapp/etc/config.phpdoesn’t help. Fix: add: intto the method andreturn Command::SUCCESS;(which is just0) on all return paths. -
04
system.xmlno longer accepts inline HTML inside<comment>2.4.9 tightened the XSD validation for
etc/adminhtml/system.xml. Earlier versions accepted<comment>Example: <code>foo</code></comment>— i.e., inline HTML tags inside<comment>. As of 2.4.9 only plain text or CDATA is allowed:<comment><![CDATA[Example: <code>foo</code>]]></comment>. Symptom:setup:upgradeaborts with “Element ‘code’: This element is not expected. Expected is ( model ).” Wrap any inline HTML in CDATA. -
05
Disabling a module in
app/etc/config.phpdoesn’t skip PHP’s autoload of its classesWhen you set
'YourVendor_Module' => 0and the module’s class is referenced anywhere (composer classmap, factory references, GraphQL schema), PHP’s autoloader still loads the class — and any PHP 8.4 deprecation inside that class still fires. The enable-flag check happens at Magento bootstrap, after PHP class loading. So if a module’s class is broken at parse time, the only way to skip it iscomposer remove. Plain English:config.phpcontrols Magento behavior; it doesn’t control PHP itself.
Step-by-step upgrade — 7 steps
Same pattern I ran on 2026-05-13. Generic — no Docker assumptions, no project-specific helpers. Copy / paste each block as-is, swap the placeholders for your values.
-
01
Take a full backup
DB dump (with routines + triggers),
pub/mediatar.gz,app/etc/env.php(gitignored — contains the crypt key!),composer.json+composer.lock. Store the tar.gz somewhere OFF this server.mysqldump --single-transaction --quick --no-tablespaces \ --routines --triggers <db_name> | gzip -9 > backups/db.sql.gz tar -czf backups/media.tar.gz -C <magento_root> pub/media cp <magento_root>/app/etc/env.php backups/env.php cp <magento_root>/composer.{json,lock} backups/What to expect: Takes 2–10 minutes depending on store size. The crypt key in
env.phpis non-negotiable — without it the DB is gibberish. -
02
Bump PHP to 8.4 or 8.5
2.4.9 minimum is PHP 8.4 (PHP 8.3 is dropped). On bare LAMP/LEMP, install the new PHP-FPM and the Magento-required extensions, then point your FPM pool + nginx
fastcgi_passat it. On Docker, bump the base image tag.# Bare LAMP/LEMP (Ubuntu / Debian) sudo apt install php8.4-fpm php8.4-{cli,mysql,gd,curl,xml,mbstring,intl,bcmath,soap,zip,imagick} sudo systemctl restart php8.4-fpm nginx # Docker # In your Dockerfile, change: # FROM php:8.3-fpm --> FROM php:8.4-fpmWhat to expect: ~10–20 minutes including FPM pool config. Run
php -vfrom inside the web user’s shell to confirm. -
03
Bump infra services (or document they’re below spec)
Adobe’s 2.4.9 supported matrix: MySQL 8.4 / MariaDB 11.8 / OpenSearch 3 / Valkey 9 (Redis 7 still works for back-compat). Production should match spec. Local dev usually works on older versions because Magento’s drivers adapt at connection time.
# Examples — adapt to your hosting # MySQL: managed cloud (RDS, Aurora, MemoryStore) → schedule maintenance window # OpenSearch: blue-green a new cluster, re-index, swap DNS # Redis → Valkey: most managed providers swap with zero downtimeWhat to expect: Plan service migrations BEFORE the Magento upgrade if production. On a clean dev box, you can skip this and patch later.
-
04
Run
composer require-commercethencomposer updateDon’t use plain
composer requirefor Magento metapackages — it errors out. Use Adobe’srequire-commercecommand (added by themagento/composer-root-update-pluginthat ships with 2.4.4+).composer require-commerce --no-update "magento/product-community-edition=2.4.9" composer update --no-interactionWhat to expect: 3–8 minutes. Pulls ~40 magento/* dependency bumps + Symfony 7.4. If your
composer.jsonhas version pins, you may have to relax them first. -
05
Run
bin/magento setup:upgrade --keep-generatedThis is where 3rd-party module breakages will surface. Read the FULL output (errors are interleaved with module names). If a module crashes here, jump to the composer-patches recipe below — or temporarily
composer removeto keep moving.bin/magento setup:upgrade --keep-generatedWhat to expect: Usually 30–90 seconds on a small store. If it aborts, read the LAST traceback line — that’s the module to patch or remove.
-
06
Apply patches for any broken 3rd-party modules
Don’t edit
vendor/files directly — they get overwritten on the nextcomposer install. Usecweagans/composer-patchesto ship reversible 3-line patches. Full recipe in the next section.# Quick install composer config allow-plugins.cweagans/composer-patches true composer require cweagans/composer-patchesWhat to expect: 5–30 minutes per broken module — mostly writing the diff. When the vendor releases a fix, you drop the patch entry from
composer.json. -
07
Smoke test
Flush cache, reindex, regenerate sitemaps (if you have a SEO module), curl 10 representative pages, test admin login, run a checkout if you can. Watch
var/log/exception.log+var/log/system.logfor fresh entries.bin/magento cache:flush bin/magento indexer:reindex curl -sI https://your-store.example/ | head -1 tail -n 50 var/log/exception.logWhat to expect: 15–30 minutes of click-testing. Look for new entries in the log files in the last 10 minutes — not old ones.
The composer-patches pattern
For unmaintained 3rd-party modules. Editing vendor/ directly is forbidden — the changes get overwritten on the next composer install. The right answer is a versioned patch file in your repo that re-applies on every install.
Why this matters: some 3rd-party vendors don’t ship PHP 8.4 fixes promptly. cweagans/composer-patches lets you ship a reversible 3-line patch that lives in your repo, applies on every composer install, and drops cleanly when the upstream vendor fixes the bug.
-
01
Install the plugin
Adobe’s own
magento/quality-patchesuses this same plugin — it’s the standard Magento pattern.composer config allow-plugins.cweagans/composer-patches true composer require cweagans/composer-patches -
02
Generate the patch with
diff -uHand-written patches go wrong at hunk 3+. Always generate via
diff -ufrom clean copies. Keep the path inside the patch relative to the package root.mkdir -p patches cp vendor/their/module/Foo.php /tmp/Foo.php.fixed # ...edit /tmp/Foo.php.fixed to fix the PHP 8.4 issue... diff -u vendor/their/module/Foo.php /tmp/Foo.php.fixed \ > patches/their-module-php84-fix.patch -
03
Register in
composer.jsonMap the patch to the package name (not the path). One package can have many patches.
{ "extra": { "patches": { "their/module": { "PHP 8.4 implicit-nullable fix": "patches/their-module-php84-fix.patch" } } } } -
04
Apply
v2.0+ of the plugin uses
patches-relock+patches-repatchcommands (NOT auto-apply on install). When the upstream vendor ships a fix, remove the entry andcomposer updateto drop the patch cleanly.composer patches-relock composer patches-repatch
Backup recipe — generic mysqldump pattern
Four files. Plain bash, no Docker assumptions. Run before you touch anything else — and test the restore on a throwaway box if you have never run a Magento DB restore before.
-
Database dump
Run on the host where MySQL is reachable.
--single-transactionavoids table locks on InnoDB;--routines --triggersincludes stored procs and triggers.TS=$(date +%Y%m%d-%H%M%S) mysqldump -u<db_user> -p<db_pass> \ --single-transaction --quick --no-tablespaces \ --routines --triggers \ <your_db_name> | gzip -9 > backups/db-${TS}.sql.gzWhy it matters: The catalog, customers, orders, product attributes, CMS pages all live here. Lose this = total restart.
-
env.php— has the crypt keyenv.phpholds the crypt key, DB credentials, queue config, cache config. Magento can’t decrypt anything in the DB without the same key.cp <magento_root>/app/etc/env.php backups/env-${TS}.php chmod 600 backups/env-${TS}.phpWhy it matters: Without the same crypt key, Magento sees customer addresses, admin passwords, and payment data as encrypted gibberish. Lose this = unrecoverable.
-
Media archive
Product images, page banners, CMS uploads. Skip this if your media is on object storage (S3, Cloudflare R2, GCS) and already versioned.
tar -czf backups/media-${TS}.tar.gz \ -C <magento_root> pub/mediaWhy it matters: Most of the disk weight. Restoring from a stale media set on rollback is fine because product images rarely change in a 3-hour upgrade window.
-
Composer files
The exact state of every installed package. Restoring lets you deterministically revert PHP-level changes — not just the Magento ones.
cp <magento_root>/composer.json backups/composer.json-${TS} cp <magento_root>/composer.lock backups/composer.lock-${TS}Why it matters: Lets you reinstall the exact pre-upgrade set of vendor packages on rollback, including the same patch versions of every 3rd-party extension.
Rollback in ~3 minutes
If the upgrade goes sideways. Plain bash + mysql + composer commands. Test this BEFORE you start the upgrade — proof that you can get back.
Restore to pre-2.4.9 state
Run this sequence with the timestamped backup files from section 6. Total time: about 3 minutes once everything is staged.
# Rollback to pre-2.4.9 state in ~3 minutes
BACKUP_DIR=backups
TS=20260513-132706
MAGENTO_ROOT=/var/www/html
# 1. Restore env.php + composer files
cp $BACKUP_DIR/env-$TS.php $MAGENTO_ROOT/app/etc/env.php
cp $BACKUP_DIR/composer.json-$TS $MAGENTO_ROOT/composer.json
cp $BACKUP_DIR/composer.lock-$TS $MAGENTO_ROOT/composer.lock
# 2. Restore DB
gunzip -c $BACKUP_DIR/db-$TS.sql.gz | mysql -u<db_user> -p<db_pass> <db_name>
# 3. Drop back to old PHP if you bumped versions
# (varies — apt remove php8.4-* / docker compose up with old PHP_VERSION)
# 4. Composer install pre-2.4.9 packages
cd $MAGENTO_ROOT && composer install --no-dev --optimize-autoloader
bin/magento setup:upgrade --keep-generated
bin/magento cache:flush
Real findings from my 2026-05-13 upgrade
All five traps from section 3 hit my own upgrade. Short version of how each one shook out.
Trap 1 (composer require rejection) hit immediately. Composer surfaced the error in 2 seconds; the fix was switching to composer require-commerce. Cost: 30 seconds of reading the error.
Trap 2 (PHP 8.4 implicit-nullable) hit on 2 separate 3rd-party modules. Both maintainers shipped fixes within hours of opening a bug report. In the meantime I held the upgrade on a feature branch and patched locally with cweagans/composer-patches. Cost: ~25 minutes per module, mostly waiting for the maintainer.
Trap 3 (Symfony 7.4 Command::execute return type) hit on 1 admin CLI module — same maintainer, same fast turnaround. Same composer-patches workaround used in the interim.
Trap 4 (system.xml XSD tightening) hit on 1 module that shipped HTML in <comment>. Wrapped in CDATA, opened a pull request upstream, moved on. Cost: 5 minutes.
Trap 5 (config.php disable doesn’t help) — I lost ~10 minutes trying to disable a module via app/etc/config.php before realising composer remove is the only escape valve when a class fails at parse time. That’s the lesson worth remembering: config.php controls Magento behavior; it doesn’t control PHP itself.
Each trap was a 5–30 minute fix once diagnosed. The full upgrade took ~3 hours total with backup + verification + the 5 hiccups. On a cleaner store (no 3rd-party modules), I’d expect this to be under an hour.
Audit YOUR 3rd-party modules before upgrading
Three grep snippets. Run them BEFORE you start the upgrade — they flag the same patterns Magento 2.4.9 will fail on, so you can fix them or open vendor bugs in advance.
-
PHP 8.4 implicit-nullable problems
Finds the
Type $param = nullpattern (without?) inside everyvendor/module except Adobe’s own. These are the modules that will fatal at autoload on 2.4.9.grep -rn -E "function [a-zA-Z_]+\([^)]*[^?]\barray \\$[a-zA-Z_]+ *= *null" \ --include="*.php" vendor/ | grep -v "vendor/magento/" -
Symfony 7.4
Command::execute()return-type problemsFinds custom CLI commands that declared
execute()without: int. These break at class load on Symfony 7.4 — before Magento even checks if the module is enabled.grep -rn "protected function execute(" --include="*.php" vendor/ \ | grep -v "vendor/magento/" \ | grep -v ": int" -
system.xml<comment>with inline HTMLFinds
<comment>elements that contain HTML tags — rejected by the 2.4.9 XSD. Wrap them in CDATA before upgrading.grep -rln "<comment>.*<[a-zA-Z][^>]*>" \ --include="system.xml" vendor/
Plain English: run these three greps BEFORE you start the upgrade. They flag the same patterns Adobe Commerce 2.4.9 will fail on. Fix them or open vendor bugs in advance — much cheaper than diagnosing during a live cutover.
Adobe supported matrix vs dev-environment reality
What Adobe says you need (left) vs what actually worked on my dev box (right). Production should still match the supported matrix — Adobe only QA-tests against documented configs.
| Component | Adobe supported (2.4.9) | What I actually ran |
|---|---|---|
| PHP | 8.4 / 8.5 | 8.4 minimum is enforced — can’t skip |
| MySQL / MariaDB | MySQL 8.4 / MariaDB 11.8 / 12.3 | Upgrade worked on MySQL 8.0 (drivers adapt) |
| Search engine | OpenSearch 3 | Upgrade worked on OpenSearch 2.12 (re-test queries) |
| Cache layer | Valkey 9 (Redis 7 deprecated) | Redis 7 still works — same wire protocol |
| Message broker | RabbitMQ 4.2 | RabbitMQ 3.13 connects fine for now |
| Composer | 2.9.3+ | Older Composer prints warnings; require-commerce still works |
| Web server | nginx 1.28 | nginx 1.24 serves Magento without complaint |
Magento 2.4.9 upgrade — frequently asked questions
-
How long does a 2.4.9 upgrade take?
1 to 2 hours for a clean store with no custom modules. 1 to 4 weeks for typical stores with 10 to 30 extensions. 4 to 12 weeks for enterprise B2B plus heavy customisation. Book a maintenance window of at least 30 minutes for the cutover itself. -
Do I need to upgrade if I am on 2.4.8-p3?
Not urgently. Adobe ships security patches for 2.4.8 through July 2027. But upgrade before mid-2027 to stay on a supported PHP version and a supported infrastructure stack. -
What is the cost?
DIY is zero dollars plus your time. A small-to-mid agency engagement is typically 2k to 15k US dollars for a mid-complexity store on 2.4.8. Enterprise B2B with heavy customisation runs 20k to 80k. Use the upgrade-cost calculator for a fast estimate. -
Can I roll back if something breaks?
Yes, in about 3 minutes if you took the backup. Restore env.php, restore the DB from the gzipped dump, restore composer.json and composer.lock, drop back to the old PHP version, then composer install. The rollback recipe is in section 7 of this guide. -
Will my Hyva storefront work?
Yes. Hyva Themes and Hyva Checkout are both 2.4.9-compatible. Re-test the Braintree adapter if you accept Google Pay or Apple Pay, because Adobe added 12+ Braintree changes in 2.4.9 including Apple Pay on Chrome and Firefox. -
Can I keep PHP 8.3?
No. Magento 2.4.9 minimum is PHP 8.4. Adobe drops 8.3. PHP 8.5 is supported. You upgrade PHP first (separate change, around 1 day), then upgrade Magento. -
Do I have to migrate Redis to Valkey?
Not immediately. Valkey speaks the Redis protocol, so your Redis 7 server still works because Magento talks to it via the same client. But Adobe's test matrix targets Valkey going forward, so plan a migration on your terms. Most cloud providers can swap with zero downtime. -
What if my paid extension does not support 2.4.9 yet?
Three options: wait for the vendor to release a 2.4.9 build, composer remove the extension and live without the feature for a while, or use cweagans/composer-patches to apply a fix you write yourself. Editing vendor/ files directly is never the right answer because the changes get overwritten on the next composer install. -
Is composer require-commerce different from composer require?
Yes. Adobe's magento/composer-root-update-plugin adds the require-commerce command to handle metapackage upgrades. Regular composer require errors out for Magento metapackages since Composer 2.1.6 because the root-package update logic is custom. -
Should I run setup:di:compile and setup:static-content:deploy?
In developer mode you usually skip both because Magento generates assets on demand. In production mode both are mandatory after every code change. For an upgrade run, switch to production mode AFTER you have confirmed all modules pass setup:upgrade cleanly. -
What about Adobe Commerce versus Magento Open Source?
Same upgrade path. Adobe Commerce ships extra modules (Companies, RMA, content staging, page builder advanced) but the core upgrade procedure is identical. Adobe Commerce stores typically need an extra 1 to 2 weeks of QA for the B2B and staging flows. -
Do I need a maintenance window?
Yes — block at least 30 minutes. Run bin/magento maintenance:enable before and bin/magento maintenance:disable after. Add your office IP to maintenance:allow-ips first so you can still browse the storefront while the upgrade runs.
Want this done for you?
I just shipped a 2.4.9 upgrade on my own store. The exact playbook is on this page; the experience is yours for the asking. Fixed-price quotes in 24 hours.