Chat on WhatsApp
Real walkthrough · May 2026

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.

TL;DR

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.

Known traps

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.

  1. 01

    composer require errors out on the metapackage

    Adobe’s magento/product-community-edition is a metapackage. Since Composer 2.1.6, composer require magento/product-community-edition=2.4.9 rejects it with “Use require-commerce instead.” The fix: Adobe ships magento/composer-root-update-plugin (bundled with 2.4.4+). Use composer require-commerce instead. Plain English: the regular composer command can’t handle Magento’s root-package update logic — Adobe gives you a custom command for it.

  2. 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 every bin/magento command crashes. Plain English: PHP 8.4 wants ?Type $param = null (explicit nullable). The old Type $param = null style is a syntax error in disguise. Detection: grep the vendor/ tree for the pattern, excluding vendor/magento/ (Adobe ships clean code).

  3. 03

    Symfony 7.4 enforces : int return on Command::execute() — old custom commands break at autoload

    Magento 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 declared execute() with no return type or : void fails LSP at class declaration. The fatal happens BEFORE Magento checks if the module is enabled — so disabling in app/etc/config.php doesn’t help. Fix: add : int to the method and return Command::SUCCESS; (which is just 0) on all return paths.

  4. 04

    system.xml no 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:upgrade aborts with “Element ‘code’: This element is not expected. Expected is ( model ).” Wrap any inline HTML in CDATA.

  5. 05

    Disabling a module in app/etc/config.php doesn’t skip PHP’s autoload of its classes

    When you set 'YourVendor_Module' => 0 and 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 is composer remove. Plain English: config.php controls Magento behavior; it doesn’t control PHP itself.

Step-by-step

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.

  1. 01

    Take a full backup

    DB dump (with routines + triggers), pub/media tar.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.php is non-negotiable — without it the DB is gibberish.

  2. 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_pass at 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-fpm

    What to expect: ~10–20 minutes including FPM pool config. Run php -v from inside the web user’s shell to confirm.

  3. 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 downtime

    What to expect: Plan service migrations BEFORE the Magento upgrade if production. On a clean dev box, you can skip this and patch later.

  4. 04

    Run composer require-commerce then composer update

    Don’t use plain composer require for Magento metapackages — it errors out. Use Adobe’s require-commerce command (added by the magento/composer-root-update-plugin that ships with 2.4.4+).

    composer require-commerce --no-update "magento/product-community-edition=2.4.9"
    composer update --no-interaction

    What to expect: 3–8 minutes. Pulls ~40 magento/* dependency bumps + Symfony 7.4. If your composer.json has version pins, you may have to relax them first.

  5. 05

    Run bin/magento setup:upgrade --keep-generated

    This 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 remove to keep moving.

    bin/magento setup:upgrade --keep-generated

    What 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.

  6. 06

    Apply patches for any broken 3rd-party modules

    Don’t edit vendor/ files directly — they get overwritten on the next composer install. Use cweagans/composer-patches to 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-patches

    What 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.

  7. 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.log for fresh entries.

    bin/magento cache:flush
    bin/magento indexer:reindex
    curl -sI https://your-store.example/ | head -1
    tail -n 50 var/log/exception.log

    What to expect: 15–30 minutes of click-testing. Look for new entries in the log files in the last 10 minutes — not old ones.

Patching pattern

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.

  1. 01

    Install the plugin

    Adobe’s own magento/quality-patches uses this same plugin — it’s the standard Magento pattern.

    composer config allow-plugins.cweagans/composer-patches true
    composer require cweagans/composer-patches
  2. 02

    Generate the patch with diff -u

    Hand-written patches go wrong at hunk 3+. Always generate via diff -u from 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
  3. 03

    Register in composer.json

    Map 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"
          }
        }
      }
    }
  4. 04

    Apply

    v2.0+ of the plugin uses patches-relock + patches-repatch commands (NOT auto-apply on install). When the upstream vendor ships a fix, remove the entry and composer update to drop the patch cleanly.

    composer patches-relock
    composer patches-repatch
Backup recipe

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-transaction avoids table locks on InnoDB; --routines --triggers includes 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.gz

    Why it matters: The catalog, customers, orders, product attributes, CMS pages all live here. Lose this = total restart.

  • env.php — has the crypt key

    env.php holds 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}.php

    Why 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/media

    Why 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 recipe

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
What I actually hit

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.

Pre-upgrade audit

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 = null pattern (without ?) inside every vendor/ 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 problems

    Finds 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 HTML

    Finds <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.

Supported matrix

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
FAQ

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&apos;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&apos;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.
Done-for-you upgrade

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.