DEV Community

Magevanta
Magevanta

Posted on • Originally published at magevanta.com

Magento 2 Setup:Upgrade Performance — Speed Up Slow Deployments

If you've ever watched bin/magento setup:upgrade run for 20 minutes on a production database while your deployment pipeline ticks away, you know the pain. On large Magento 2 installations with hundreds of modules and millions of records, setup:upgrade becomes the single biggest bottleneck in your CI/CD pipeline — often taking longer than the actual code deployment, static content generation, and cache warming combined.

The good news? Most of that time is wasted on operations you can optimize, skip, or parallelize. Let's break down exactly what setup:upgrade does, where it spends its time, and how to make it fast.

What Setup:Upgrade Actually Does

setup:upgrade is Magento's schema and data migration tool. When you deploy new code, it:

  1. Checks module versions — reads every module's module.xml and db_schema.xml
  2. Runs schema upgrades — executes InstallSchema, UpgradeSchema, and declarative schema patches
  3. Runs data patches — executes RecurringDataPatch and module-level data patches
  4. Updates setup_module table — records the new module version
  5. Runs DI compilation — regenerates generated/ (if in production mode)
  6. Flushes cache types — invalidates configs, layout, block_html, full_page

Steps 2 and 3 are where most of the time goes. If you have modules with poorly written data patches, N+1 queries in upgrade scripts, or schema operations that don't use batch inserts, setup:upgrade can crawl.

Step 1: Profile Before You Optimize

Don't guess — measure. Run setup:upgrade with the verbose flag and capture timestamps:

bin/magento setup:upgrade --verbose 2>&1 | while IFS= read -r line; do
  echo "$(date '+%H:%M:%S') $line"
done | tee /tmp/setup-upgrade.log
Enter fullscreen mode Exit fullscreen mode

This gives you a timestamped log showing exactly which modules take the longest. Look for:

  • Long gaps between module lines — a single module's upgrade is slow
  • Repeating patterns — a third-party module doing per-row inserts in a loop
  • "Data upgrade..." messages — data patches running row-by-row instead of in bulk

For a deeper dive, enable MySQL's slow query log during the upgrade:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
Enter fullscreen mode Exit fullscreen mode

Run setup:upgrade, then check mysql.slow_log for queries taking more than a second. You'll often find the same few queries repeated hundreds of times.

Step 2: Eliminate Unnecessary Module Upgrades

Not every deployment changes every module. If only 3 modules changed, why upgrade all 300?

Magento doesn't give you a native --only-module flag, but you can work around it. First, identify which modules actually changed version:

# Compare deployed versions vs new code versions
cd /var/www/html
bin/magento module:status --enabled | while read module; do
  code_version=$(grep -oP 'setup_version="[0-9.]+"' app/code/*/etc/module.xml 2>/dev/null | grep -oP '[0-9.]+' | head -1)
  db_version=$(bin/magento setup:db:status --module="$module" 2>/dev/null | grep -oP 'version: [0-9.]+' | grep -oP '[0-9.]+')
  [ "$code_version" != "$db_version" ] && echo "$module: $db_version$code_version"
done
Enter fullscreen mode Exit fullscreen mode

If fewer than 10 modules changed, consider running schema patches manually or using a targeted approach. For full automation, build this check into your deploy script and skip setup:upgrade entirely when no modules changed version.

Step 3: Optimize Data Patches

Data patches (Patch/Schema and Patch/Data) are the #1 culprit for slow upgrades. Common anti-patterns:

Anti-pattern 1: Per-row inserts in a loop

// BAD — runs 50,000 queries
foreach ($productIds as $id) {
    $this->connection->insert('catalog_product_entity_int', [
        'attribute_id' => $attrId,
        'entity_id' => $id,
        'value' => 1,
    ]);
}
Enter fullscreen mode Exit fullscreen mode
// GOOD — single batch insert
$rows = array_map(function ($id) use ($attrId) {
    return ['attribute_id' => $attrId, 'entity_id' => $id, 'value' => 1];
}, $productIds);
$this->connection->insertMultiple('catalog_product_entity_int', $rows);
Enter fullscreen mode Exit fullscreen mode

Anti-pattern 2: Loading full collections

// BAD — loads 50K products into memory
$products = $this->productCollectionFactory->create()->load();
foreach ($products as $product) { ... }
Enter fullscreen mode Exit fullscreen mode
// GOOD — use direct SQL or iterator
$select = $this->connection->select()->from('catalog_product_entity', ['entity_id']);
$iterator = $this->connection->query($select);
while ($batch = $this->fetchBatch($iterator, 1000)) { ... }
Enter fullscreen mode Exit fullscreen mode

Anti-pattern 3: Missing dependency declarations

If PatchB depends on PatchA, but the dependency isn't declared, Magento may run them in parallel or in the wrong order. The patch system uses getDependencies() to sequence execution. Missing dependencies cause deadlocks on large installs.

public static function getDependencies()
{
    return [
        \Vendor\Module\Setup\Patch\Data\AddColumnToProducts::class,
    ];
}
Enter fullscreen mode Exit fullscreen mode

Audit your third-party modules' data patches — you'll be surprised how many have these anti-patterns. If vendor patches are slow, consider wrapping them or running them pre-deployment during off-peak hours.

Step 4: Mark Already-Applied Patches (Skip Redundant Work)

If you know certain data patches have already run and don't need to run again, you can mark them as applied in the database:

INSERT INTO setup_patches SET patch_name = 'Vendor\\Module\\Setup\\Patch\\Data\\SlowPatch',
  patch_type = 'data', patch_version = '1.0.0', patch_id = NULL
ON DUPLICATE KEY UPDATE patch_name = patch_name;
Enter fullscreen mode Exit fullscreen mode

This skips the patch on the next setup:upgrade run. Only do this if you're certain the patch has been applied — check with your deployment logs first.

For a safer approach, use Magento's --safe-mode=1 flag during staging deployments to verify patches before running them in production.

Step 5: Declarative Schema Over Upgrade Scripts

Declarative schema (db_schema.xml) is significantly faster than UpgradeSchema.php scripts because Magento generates a single diff and applies it in batch, rather than running arbitrary PHP per module.

If you maintain custom modules, migrate from UpgradeSchema.php to db_schema.xml. The migration is straightforward:

  1. Create etc/db_schema.xml matching your current schema
  2. Add <schema> declaration in module.xml
  3. Run bin/magento setup:upgrade --convert-old-scripts=1 to auto-generate the schema from existing upgrade scripts
  4. Review and test the generated db_schema.xml

Magento handles the diff, generates CREATE TABLE, ALTER TABLE, and DROP TABLE statements automatically. No PHP execution needed per upgrade cycle — Magento compares the declarative schema against the database and only runs the delta.

Step 6: Skip DI Compilation During Upgrade

If you generate compiled DI separately (in your CI pipeline), you don't need setup:upgrade to do it. Run:

bin/magento setup:upgrade --keep-generated
Enter fullscreen mode Exit fullscreen mode

This skips the generated/ regeneration step, which can save several minutes on large installs. Just make sure you run setup:di:compile in your CI pipeline before deploying:

# In your CI pipeline
bin/magento setup:di:compile
# Deploy the generated/ directory with the code

# On the server, during deploy hook
bin/magento setup:upgrade --keep-generated
Enter fullscreen mode Exit fullscreen mode

Important: Only use --keep-generated if you've already compiled DI for the exact code version being deployed. Mismatched generated code causes runtime errors.

Step 7: Parallelize Where Possible

Magento's setup:upgrade runs modules sequentially. While you can't natively parallelize it, you can:

  1. Run schema patches and DI compilation in parallel — schema patches finish first, while DI compilation runs in a separate process
  2. Pre-warm cache during upgrade — use a background process to warm full page cache while upgrade runs
  3. Use maintenance mode strategically — put the site in maintenance, run upgrade, then take it out. Don't leave the site live during upgrade.
bin/magento maintenance:enable
bin/magento setup:upgrade --keep-generated &
bin/magento setup:di:compile &
wait
bin/magento cache:clean
bin/magento maintenance:disable
Enter fullscreen mode Exit fullscreen mode

Step 8: Database-Level Optimizations

The setup:upgrade process is I/O-bound on MySQL. These database-side optimizations make a measurable difference:

  • Increase innodb_buffer_pool_size — ensure the entire setup_module and setup_patches tables fit in memory
  • Tune innodb_flush_log_at_trx_commit — setting to 2 during maintenance windows gives a 2-3x write speedup at the cost of ACID guarantees (safe for deployment windows)
  • Drop unused indexes before upgrade — if a data patch recreates indexes, dropping them first and recreating after is faster than incremental updates
  • Increase innodb_io_capacity — during maintenance, allow MySQL to use more I/O throughput for faster table operations
-- Temporarily boost InnoDB performance during maintenance
SET GLOBAL innodb_flush_log_at_trx_commit = 2;
SET GLOBAL innodb_io_capacity = 2000;
SET GLOBAL innodb_io_capacity_max = 4000;

-- After upgrade completes
SET GLOBAL innodb_flush_log_at_trx_commit = 1;
SET GLOBAL innodb_io_capacity = 200;
SET GLOBAL innodb_io_capacity_max = 2000;
Enter fullscreen mode Exit fullscreen mode

Remember to reset these values after the upgrade completes. Running with relaxed durability settings permanently risks data loss on power failure.

Step 9: Audit Third-Party Modules

Third-party modules often ship with heavy upgrade scripts. Common offenders:

  • ERP integrations — creating custom tables with initial data seed
  • Payment gateways — adding columns to sales_order with default values on large tables
  • Search extensions — rebuilding entire index tables during upgrade

Audit the Setup/Patch/ directories of your installed modules. If you find slow patches, contact the vendor or patch them locally:

# Find all data patches across vendor modules
find vendor/ -path '*/Setup/Patch/Data/*.php' -exec basename {} \;
Enter fullscreen mode Exit fullscreen mode

For the worst offenders, you can often refactor the patch to use batch operations and submit a PR upstream. Many vendors are responsive to performance PRs, especially if you include benchmarks showing the before/after timings.

Step 10: Track Upgrade Duration Over Time

Once you've optimized, keep an eye on regressions. Log setup:upgrade duration on every deployment:

start=$(date +%s)
bin/magento setup:upgrade --keep-generated
end=$(date +%s)
duration=$((end - start))
echo "$(date -Iseconds) setup_upgrade_duration=${duration}s" >> /var/log/magento-deployments.log
Enter fullscreen mode Exit fullscreen mode

Graph this in Grafana, Datadog, or your monitoring tool of choice. When a new deployment suddenly takes 2x longer, you'll know immediately which module introduced a slow patch. Set up alerts for when upgrade duration exceeds your baseline by more than 50%.

Conclusion

setup:upgrade doesn't have to be the bottleneck in your deployment pipeline. By profiling what's slow, optimizing data patches, migrating to declarative schema, skipping unnecessary work, and parallelizing where you can, large Magento 2 installs can go from 20-minute upgrades to under 5 minutes.

The key takeaways:

  • Profile first — don't optimize blind, use verbose logging and MySQL slow query log
  • Batch everything — per-row operations in data patches are the #1 performance killer
  • Use --keep-generated — skip DI compilation during upgrade if it's done in CI
  • Migrate to declarative schema — it's faster, declarative, and auto-generates diffs
  • Audit third-party modules — vendor patches are often the worst offenders
  • Track duration over time — catch regressions before they become deployment-day emergencies

Fast deployments mean more deployments. More deployments mean smaller changes, faster feedback, and less risk. Every minute you shave off setup:upgrade pays dividends across your entire development workflow.

Top comments (0)