Chat on WhatsApp
Magento Development 13 min read

What Magento setup:upgrade Actually Does (In Depth)

On the surface setup:upgrade just “applies updates.” Underneath it is an ordered pipeline that reconciles your database with your code. Here is exactly what runs, in what order, and which tables it writes.

What Magento setup:upgrade Actually Does (In Depth)

setup:upgrade is the command every Magento developer runs a hundred times — and the one almost nobody can fully explain. On the surface it “applies updates.” Underneath, it is a carefully ordered pipeline that brings your database into agreement with the code currently on disk.

This guide walks through exactly what happens when you press enter: which phases run, in what order, which tables get written, what gets skipped, and why the same command behaves differently in developer mode versus production. By the end you will be able to read a failed upgrade and know precisely which phase broke.

2Schema engines (declarative + legacy)
3Patch / script types it runs
2Tables that track its work
0Static assets or DI it compiles

The short answer

Running bin/magento setup:upgrade tells Magento: “here is the code that is now on disk — make the database match it.” It discovers and enables modules, applies any schema differences, runs schema and data patches that have not run yet, executes recurring scripts, stamps each module’s version into setup_module, and clears configuration cache. It is idempotent by design: run it twice in a row and the second run does almost nothing, because Magento records what it already completed.

What setup:upgrade runs, in order

Internally the command is handled by Magento’s Installer. For an existing store (an upgrade rather than a fresh install) the phases are:

  1. Bootstrap & module discovery. Magento reads the enabled-module list from app/etc/config.php, scans every registration.php under app/code and vendor, and resolves load order from each module.xml <sequence>.
  2. Enable new modules. Any module present on disk but missing from config.php is added with 1 (enabled). This is why a brand-new module “turns on” the first time you upgrade.
  3. Schema phase. Declarative schema is applied first (see below), then legacy InstallSchema (first install) / UpgradeSchema (when the module version increased), then every SchemaPatchInterface patch that has not run, then Recurring.php schema logic.
  4. Data phase. Legacy InstallData/UpgradeData (version-gated), then every DataPatchInterface patch that has not run, then recurring data logic.
  5. Version bookkeeping. Each module’s schema_version and data_version in setup_module are updated to the current code version.
  6. Cache invalidation. Configuration and related caches are flushed so the new module config, routes, and DI wiring are picked up on the next request.
PhaseWhat actually happensWhere it is tracked
Module enableNew modules written as enabledapp/etc/config.php
Declarative schemaLive DB diffed against every db_schema.xml; delta appliedThe database itself + db_schema_whitelist.json
Legacy schema scriptsUpgradeSchema runs if code version > setup_module.schema_versionsetup_module.schema_version
Schema patchesSchemaPatchInterface classes run oncepatch_list
Data scripts / patchesUpgradeData (version-gated) + DataPatchInterface (once)setup_module.data_version + patch_list
RecurringRecurring.php runs every time, regardless of versionNot tracked — always executes

Behind the scenes: the full lifecycle, start to finish

Run the command with -vvv and Magento prints its real phase order. On this store that means walking the 47 modules tracked in setup_module and checking 936 entries in patch_list to decide what is new. The terminal trace is the lifecycle:

Cleaning up deprecated SET NAMES utf8 from database connections...
[SUCCESS] Cache types config flushed / Cache cleared
File system cleanup:        # clears generated/code (and generated/metadata in production)
Updating modules:          # module registry -> app/etc/config.php + setup_module
Schema creation/updates:   # declarative-schema diff + Install/UpgradeSchema, per module, in dependency order
Schema post-updates:       # SchemaPatchInterface patches + Recurring (schema)
[SUCCESS] Cache cleared
Data install/update:       # InstallData/UpgradeData + DataPatchInterface (recorded in patch_list)
Data post-updates:         # Recurring (data)
Upgrade completed successfully.

Read top to bottom, the background process is: (1) flush configuration so the new module list is seen; (2) clean stale generated code; (3) register and enable modules, resolving load order from each module.xml <sequence>; (4) the schema phase — declarative diff first, then legacy schema scripts, then schema patches, module by module in dependency order; (5) the data phase — legacy data scripts, then every data patch not yet in patch_list; (6) recurring scripts, which run every single time; (7) stamp each module’s schema_version and data_version and clear cache. The key ordering guarantee: the entire schema phase finishes before the data phase starts, so a data patch can safely rely on a column a schema patch just created.

Declarative schema vs the old Install/Upgrade scripts

Since Magento 2.3, the primary way to define database structure is declarative schema. Instead of writing imperative “create this column” PHP, you describe the desired end state in etc/db_schema.xml:

<!-- app/code/Panth/Example/etc/db_schema.xml -->
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <table name="panth_example_item" resource="default" engine="innodb">
    <column xsi:type="int" name="item_id" padding="10" unsigned="true"
            nullable="false" identity="true"/>
    <column xsi:type="varchar" name="title" nullable="false" length="255"/>
    <constraint xsi:type="primary" referenceId="PRIMARY">
      <column name="item_id"/>
    </constraint>
  </table>
</schema>

On setup:upgrade, Magento builds the target schema from every module’s db_schema.xml, reads the current schema straight from the database, computes the difference, and runs only the required CREATE/ALTER/DROP statements. Because it diffs the real database, declarative schema is not version-gated — remove a column from the XML and it is dropped on the next upgrade. The db_schema_whitelist.json file (generated by setup:db-declaration:generate-whitelist) tells Magento which objects it is allowed to drop, so it never deletes something it did not create.

Key takeaway: declarative schema is a diff, not a migration history. The legacy InstallSchema/UpgradeSchema/Recurring scripts still run for back-compat, but new code should use db_schema.xml for structure and patches for data.

Schema patches and data patches

Patches replaced the old version-number-driven upgrade scripts with self-describing, dependency-ordered classes. A data patch implements DataPatchInterface and lives in Setup/Patch/Data/:

<?php
namespace Panth\Example\Setup\Patch\Data;

use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;

class SeedDefaultItems implements DataPatchInterface
{
    public function __construct(
        private ModuleDataSetupInterface $moduleDataSetup
    ) {}

    public function apply(): void
    {
        $this->moduleDataSetup->getConnection()->insert(
            $this->moduleDataSetup->getTable('panth_example_item'),
            ['title' => 'First item']
        );
    }

    public static function getDependencies(): array { return []; }
    public function getAliases(): array { return []; }
}

When setup:upgrade reaches the data phase, it loads every DataPatchInterface class, sorts them by getDependencies(), and runs each one whose class name is not already in the patch_list table. After a patch succeeds, its fully-qualified class name is inserted into patch_list so it never runs again. getAliases() lets you rename or move a patch class without it re-running. Schema patches work identically but implement SchemaPatchInterface and run in the schema phase.

The two tables that control everything

Almost every “why didn’t my change apply?” question comes down to these two tables.

setup_module — one row per module with its installed schema_version and data_version. Legacy UpgradeSchema/UpgradeData only run when the code’s version is higher than the stored version:

SELECT module, schema_version, data_version
FROM setup_module
WHERE module = 'Panth_Example';

patch_list — one row per applied schema/data patch, keyed by class name:

SELECT patch_name FROM patch_list
WHERE patch_name LIKE 'Panth\\\\Example%';

If you edit a data patch that has already run, nothing happens on the next upgrade — its name is in patch_list. To force it to re-run (for example, to re-seed content), delete its row first:

DELETE FROM patch_list WHERE patch_name LIKE '%SeedDefaultItems';
bin/magento setup:upgrade --keep-generated
Key takeaway: patches are meant to be write-once and idempotent. If you find yourself deleting patch_list rows often, make the patch itself idempotent (check-before-insert) instead.

What setup:upgrade does NOT do

This is the single biggest source of confusion. setup:upgrade only reconciles the database and module registry. It does not:

  • Compile dependency injection — that is setup:di:compile (required in production mode).
  • Deploy static view files — that is setup:static-content:deploy (required in production mode, or pages render unstyled).
  • Reindex — that is indexer:reindex. New attributes or data may need an index rebuild to appear on the storefront.

In developer mode you often get away with just setup:upgrade because generated code and static files are created on demand. In production mode you must run the full sequence (below) or the storefront breaks.

--keep-generated and the other flags

By default, in production mode, setup:upgrade clears the generated/ directory (compiled DI, proxies, interceptors, factories) so it can be regenerated. The flags you will actually use:

  • --keep-generated — do not wipe generated/. Use it when you have already run di:compile in a build pipeline and do not want the upgrade to throw it away.
  • --dry-run=1 — declarative schema only: print the SQL it would run without touching the database. Excellent for reviewing a risky upgrade.
  • --safe-mode=1 / --data-restore=1 — declarative schema safe mode: back up data from columns/tables about to be removed, and restore it later.
  • --convert-old-scripts=1 — generate db_schema.xml from your legacy InstallSchema/UpgradeSchema scripts.

Developer vs production mode (and maintenance mode)

The command itself is the same, but the surrounding requirements differ. In developer mode, missing generated classes and static files are built lazily on the next request, so setup:upgrade + cache:flush is usually enough. In production mode, nothing is generated on the fly — you must explicitly compile DI and deploy static content.

One thing setup:upgrade does not do for you is put the store into maintenance. On a live site, schema changes mid-traffic can throw errors for shoppers. Always wrap a production upgrade in maintenance mode yourself.

The production-safe run order

On a live store, run the full sequence — not just setup:upgrade:

bin/magento maintenance:enable
bin/magento setup:upgrade --keep-generated
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f en_US
bin/magento cache:flush
bin/magento maintenance:disable

If your deployment compiles DI and static content in a separate build step (the recommended “build → deploy” pattern), keep --keep-generated so the upgrade does not discard your pre-built generated/ directory.

Common setup:upgrade errors and how to read them

Proxy.php: No such file or directory during di:compile. The generated/ tree is half-built and the optimized autoloader references a class that is not there. Recover with a two-pass autoload: composer dump-autoload (non-optimized) → setup:di:compilecomposer dump-autoload --optimize.

SQLSTATE[HY000]: 1419 ... SUPER privilege. The upgrade is trying to create a trigger (often for indexer MView) but binary logging is on without trusted function creators. Set log_bin_trust_function_creators=1 on MySQL.

“My data patch edit did nothing.” Its class name is already in patch_list. Delete that row (or bump it via getAliases()) and re-run.

Declarative schema whitelist errors. You changed db_schema.xml but did not regenerate the whitelist. Run bin/magento setup:db-declaration:generate-whitelist --module-name=Panth_Example and commit the updated db_schema_whitelist.json.

An upgrade that won’t complete, a patch that won’t apply, or a broken generated/ tree? I debug Magento 2 & Adobe Commerce upgrades and deployments end to end — fixed-fee from $499 audit · $2,499 sprint · ~Nh @ $25/hr. See Magento upgrade service.

Get your upgrade unblocked

Frequently asked questions

What does bin/magento setup:upgrade actually do?

It reconciles your database with the code on disk: enables new modules, applies declarative and legacy schema changes, runs schema and data patches that have not run yet, executes recurring scripts, records each module’s version in setup_module, and flushes configuration cache. It does not compile DI, deploy static files, or reindex.

Does setup:upgrade reindex or deploy static content?

No. setup:upgrade only touches the database and module registry. You still need setup:di:compile and setup:static-content:deploy in production, and indexer:reindex if new data must appear on the storefront.

Why didn’t my data patch run again after I edited it?

Magento records every applied patch by class name in the patch_list table and never runs it twice. To force a re-run, delete its row: DELETE FROM patch_list WHERE patch_name LIKE '%YourPatch'; then run setup:upgrade again. Better: make the patch idempotent so re-running is safe.

What is the difference between declarative schema and InstallSchema?

Declarative schema (db_schema.xml, Magento 2.3+) describes the desired end state and Magento diffs it against the live database to apply only the needed changes. InstallSchema/UpgradeSchema are the older imperative PHP scripts, gated by module version. Both still run, but new code should use declarative schema.

What does the --keep-generated flag do?

By default, in production mode, setup:upgrade wipes the generated/ directory (compiled DI, proxies, interceptors). --keep-generated preserves it — use it when you have already compiled DI in a separate build step and don’t want the upgrade to throw that work away.

Should I enable maintenance mode before setup:upgrade?

Yes, on a live store. setup:upgrade does not enable maintenance for you, and schema changes during live traffic can throw errors for shoppers. Run bin/magento maintenance:enable first and maintenance:disable after the full deploy sequence.

Which tables does setup:upgrade write to?

Two control tables matter most: setup_module (each module’s installed schema and data version) and patch_list (the class names of schema/data patches that have already run). It also applies whatever structural and data changes your modules define.

Is it safe to run setup:upgrade twice?

Yes. It is idempotent: declarative schema diffs to a no-op when the DB already matches, version-gated scripts are skipped, and patches already in patch_list do not re-run. A second consecutive run does almost nothing.

Why does setup:upgrade fail with a 1419 SUPER privilege error?

It is trying to create a database trigger (commonly for indexer MView) while binary logging is enabled without trusted function creators. Set log_bin_trust_function_creators=1 in your MySQL configuration and re-run.