<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Magevanta</title>
    <description>The latest articles on DEV Community by Magevanta (@magevanta).</description>
    <link>https://dev.to/magevanta</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3887629%2F7af145fd-03e9-4362-99dc-a5f637f09ce1.png</url>
      <title>DEV Community: Magevanta</title>
      <link>https://dev.to/magevanta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/magevanta"/>
    <language>en</language>
    <item>
      <title>Magento 2 Third-Party Extension Performance Audit: Find and Fix Hidden Bottlenecks</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Mon, 01 Jun 2026 09:03:06 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-third-party-extension-performance-audit-find-and-fix-hidden-bottlenecks-35fg</link>
      <guid>https://dev.to/magevanta/magento-2-third-party-extension-performance-audit-find-and-fix-hidden-bottlenecks-35fg</guid>
      <description>&lt;p&gt;Third-party extensions are a double-edged sword. They add capabilities that would take months to build in-house, but every additional module adds plugins, observers, layout blocks, and database queries to every page load. A store with 40 extensions installed could easily have 200+ extra interceptors firing on a single request — and most developers have no idea which ones are costing them.&lt;/p&gt;

&lt;p&gt;This guide walks you through a systematic performance audit of your extension stack: how to measure the real cost of each module, what patterns cause the most damage, and how to fix (or quarantine) the worst offenders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Extensions Are the #1 Performance Culprit
&lt;/h2&gt;

&lt;p&gt;Before blaming infrastructure, consider that Magento's plugin (interceptor) system means any third-party module can silently wrap any public method in the entire core. When you install 10 extensions, each with 5–15 plugins, you can easily end up with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;100+ interceptors&lt;/strong&gt; compiled into generated code&lt;/li&gt;
&lt;li&gt;Additional database queries per request (EAV attribute loads, config reads, custom tables)&lt;/li&gt;
&lt;li&gt;Extra layout XML blocks rendering widgets or tracking scripts&lt;/li&gt;
&lt;li&gt;Observers attached to frequently-fired events like &lt;code&gt;sales_quote_collect_totals_before&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The worst part: these costs accumulate invisibly. No single extension looks bad in isolation, but together they drag TTFBs from 200ms to 2+ seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Baseline Your Current State
&lt;/h2&gt;

&lt;p&gt;Before auditing anything, establish a baseline. You need hard numbers to compare against.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Enable built-in profiler&lt;/span&gt;
php bin/magento dev:query-log:enable
php bin/magento cache:clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a simple shell script to benchmark a representative page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;1..5&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{time_total}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"https://yourstore.com/some-product.html"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Record your &lt;strong&gt;median&lt;/strong&gt; response time for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Homepage&lt;/li&gt;
&lt;li&gt;Category page (with layered nav)&lt;/li&gt;
&lt;li&gt;Product page&lt;/li&gt;
&lt;li&gt;Checkout cart page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also grab a database query count baseline from &lt;code&gt;var/log/db.log&lt;/code&gt; or your APM tool. Knowing you're at 180 queries per page before the audit gives you something to beat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: List Every Installed Extension
&lt;/h2&gt;

&lt;p&gt;Get a clean list of all non-Magento modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php bin/magento module:status &lt;span class="nt"&gt;--enabled&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"^Magento_"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"^List"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"^-"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each module, note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vendor name and version&lt;/li&gt;
&lt;li&gt;Last update date (stale = red flag)&lt;/li&gt;
&lt;li&gt;Purpose (payment, shipping, analytics, CMS, etc.)&lt;/li&gt;
&lt;li&gt;Whether it's business-critical or optional&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modules that haven't been updated in 2+ years deserve extra scrutiny. They often rely on deprecated APIs and haven't been optimized for modern PHP or Magento versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Profile with Blackfire or the Built-in Profiler
&lt;/h2&gt;

&lt;p&gt;The fastest way to see which extensions are expensive is to profile a real request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: Blackfire.io (Recommended)
&lt;/h3&gt;

&lt;p&gt;Install the Blackfire agent and PHP probe on your staging server. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;blackfire curl https://yourstore.com/catalog/category/view/id/42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Blackfire UI, expand the call tree and look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any vendor namespace taking &amp;gt;5% of wall time&lt;/li&gt;
&lt;li&gt;Deep interceptor chains (Plugin &amp;gt; Plugin &amp;gt; Plugin...)&lt;/li&gt;
&lt;li&gt;Repeated identical DB queries triggered from observers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Option B: Magento Built-in Profiler
&lt;/h3&gt;

&lt;p&gt;Enable the Magento profiler in &lt;code&gt;env.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'profiler'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'class'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'\Magento\Framework\Profiler\Driver\Standard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'output'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'html'&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via env variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MAGE_PROFILER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;html php &lt;span class="nt"&gt;-S&lt;/span&gt; localhost:8080 &lt;span class="nt"&gt;-t&lt;/span&gt; pub/ pub/index.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This outputs a timing table at the bottom of each page showing which blocks, layouts, and observers are slow. It's less granular than Blackfire but zero-cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option C: New Relic APM
&lt;/h3&gt;

&lt;p&gt;If you have New Relic, filter transactions by slowest segments and expand to the code-level trace. Extension methods show up with their full class path, making identification trivial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Identify the Worst Offenders
&lt;/h2&gt;

&lt;p&gt;After profiling, patterns to look for:&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Plugin Chains on Critical Methods
&lt;/h3&gt;

&lt;p&gt;Expensive plugin chains almost always appear on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Magento\Catalog\Model\Product::getPrice
Magento\Quote\Model\Quote\Item\AbstractItem::calcRowTotal
Magento\Checkout\Model\Session::getQuote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If 3–4 extensions each add an &lt;code&gt;around&lt;/code&gt; plugin to &lt;code&gt;getPrice&lt;/code&gt;, the interception overhead compounds. Check &lt;code&gt;generated/code/&lt;/code&gt; for long plugin chains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Count plugins on a specific class&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"aroundGetPrice&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;beforeGetPrice&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;afterGetPrice"&lt;/span&gt; generated/code/ | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More than 5–6 plugins on the same method is a warning sign.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Observer Storms on Quote Events
&lt;/h3&gt;

&lt;p&gt;The worst offenders are often marketing/analytics extensions that attach to &lt;code&gt;sales_quote_collect_totals_*&lt;/code&gt;, &lt;code&gt;checkout_cart_product_add_after&lt;/code&gt;, or &lt;code&gt;catalog_product_load_after&lt;/code&gt;. These events fire multiple times per request on the cart page.&lt;/p&gt;

&lt;p&gt;Find all observers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;event name="&lt;/span&gt; vendor/&lt;span class="k"&gt;*&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;/etc/events.xml app/code/&lt;span class="k"&gt;*&lt;/span&gt;/etc/events.xml 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"Magento"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;: &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows you which modules register the most event observers across the board.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Layout Block Injection on Every Page
&lt;/h3&gt;

&lt;p&gt;Some extensions inject blocks into the default layout handle, meaning their block is instantiated on &lt;strong&gt;every&lt;/strong&gt; page even if it doesn't render anything visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;handle name=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;default&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; vendor/ app/code/ 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"Magento"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even a block that renders nothing still triggers &lt;code&gt;__construct&lt;/code&gt;, config reads, and occasionally DI-heavy factory calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 Config Scoped Reads Without Cache
&lt;/h3&gt;

&lt;p&gt;Look for extensions calling &lt;code&gt;Magento\Framework\App\Config\ScopeConfigInterface::getValue&lt;/code&gt; in hot paths without memoization. Every uncached config read hits the config layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Rough count of config reads per vendor&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"scopeConfig-&amp;gt;getValue"&lt;/span&gt; vendor/ &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"Magento/"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;/ &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Fix Strategies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 Disable Non-Essential Modules for Testing
&lt;/h3&gt;

&lt;p&gt;The fastest way to validate an extension is causing slowdown: disable it and retest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php bin/magento module:disable Vendor_ModuleName
php bin/magento cache:flush
&lt;span class="c"&gt;# benchmark again&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If response time drops by &amp;gt;10% with a single module disabled, you've found a major culprit. You can then decide whether to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace it with a lighter alternative&lt;/li&gt;
&lt;li&gt;Optimize the specific slow path&lt;/li&gt;
&lt;li&gt;Keep it but confine its scope&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5.2 Replace &lt;code&gt;around&lt;/code&gt; Plugins with &lt;code&gt;before&lt;/code&gt;/&lt;code&gt;after&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;If you control the extension code, &lt;code&gt;around&lt;/code&gt; plugins are the most expensive interceptor type — they wrap the entire original method and prevent call chain optimizations. Wherever possible, convert to &lt;code&gt;before&lt;/code&gt; or &lt;code&gt;after&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Instead of this:&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;aroundGetPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;\Magento\Catalog\Model\Product&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;callable&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;float&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMultiplier&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Do this:&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;afterGetPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;\Magento\Catalog\Model\Product&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;float&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMultiplier&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially impactful when the plugin fires in a loop (e.g., iterating over cart items).&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 Add Memoization to Repeated Method Calls
&lt;/h3&gt;

&lt;p&gt;Many extensions calculate the same value (a config flag, a customer group check) dozens of times per request without caching the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyPlugin&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;?bool&lt;/span&gt; &lt;span class="nv"&gt;$isEnabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;afterGetPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Product&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;isEnabled&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;isEnabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;scopeConfig&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isSetFlag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'vendor/module/enabled'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;isEnabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// ... expensive logic&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;code&gt;private ?type $cached = null;&lt;/code&gt; pattern to any value read inside a frequently-called plugin or observer.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.4 Move Heavy Logic to Async or Cron
&lt;/h3&gt;

&lt;p&gt;Analytics tracking, loyalty point calculations, and marketing syncs don't need to happen during the customer's request. Push them to a message queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Instead of direct processing in observer:&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Magento\Framework\Event\Observer&lt;/span&gt; &lt;span class="nv"&gt;$observer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;messageQueue&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'vendor.analytics.track'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'event'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'product_view'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'product_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$observer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getProduct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s1"&gt;'customer_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customerSession&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCustomerId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The queue consumer handles it asynchronously, keeping the customer-facing request lean.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.5 Scope Layout XML to Specific Handles
&lt;/h3&gt;

&lt;p&gt;If an extension's block only makes sense on the cart page, restrict it to the &lt;code&gt;checkout_cart_index&lt;/code&gt; layout handle — not &lt;code&gt;default&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- vendor/Module/view/frontend/layout/checkout_cart_index.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;page&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="na"&gt;xsi:noNamespaceSchemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;referenceContainer&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;block&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"Vendor\Module\Block\Widget"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"vendor_widget"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/referenceContainer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/page&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This alone can shave 10–30ms on every non-cart page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Ongoing Monitoring
&lt;/h2&gt;

&lt;p&gt;A one-time audit isn't enough — extensions get updated, new ones get installed, and performance regressions creep in silently. Build these habits:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benchmark on every deploy.&lt;/strong&gt; Add a simple curl-based response time check to your CI pipeline. Alert if median TTFB increases by more than 15% on key pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review the generated interceptor file on each module install.&lt;/strong&gt; After &lt;code&gt;php bin/magento setup:di:compile&lt;/code&gt;, check what's new in &lt;code&gt;generated/code/Magento/&lt;/code&gt; for your most critical classes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit before you install.&lt;/strong&gt; Before adding any extension from the Marketplace, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many plugins does it register? (&lt;code&gt;grep -r "type name=" etc/di.xml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Does it modify high-traffic events?&lt;/li&gt;
&lt;li&gt;Is it actively maintained?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick Reference: Audit Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Red Flag&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Response time baseline&lt;/td&gt;
&lt;td&gt;curl / wrk&lt;/td&gt;
&lt;td&gt;&amp;gt;500ms TTFB on product page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin count per critical class&lt;/td&gt;
&lt;td&gt;grep generated/&lt;/td&gt;
&lt;td&gt;&amp;gt;6 plugins on same method&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observer count per event&lt;/td&gt;
&lt;td&gt;grep events.xml&lt;/td&gt;
&lt;td&gt;&amp;gt;3 observers on quote events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default layout injection&lt;/td&gt;
&lt;td&gt;grep layout XML&lt;/td&gt;
&lt;td&gt;Any non-Magento block in default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stale extensions&lt;/td&gt;
&lt;td&gt;composer show&lt;/td&gt;
&lt;td&gt;Last update &amp;gt;2 years ago&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB queries per page&lt;/td&gt;
&lt;td&gt;query log / Blackfire&lt;/td&gt;
&lt;td&gt;&amp;gt;150 queries on category page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Third-party extension audits aren't glamorous, but they're often the highest-ROI performance work you can do on an established Magento 2 store. A single badly-written analytics plugin wrapping a core method can cost you 300ms per request across your entire catalog.&lt;/p&gt;

&lt;p&gt;The process is straightforward: baseline, profile, identify, fix, and monitor. Start with your five slowest pages, trace the top three timing contributors in each, and work down the list. You'll likely find that 20% of your extensions are causing 80% of your extension-related overhead — and most of those issues have clean, targeted solutions.&lt;/p&gt;

&lt;p&gt;Combine this audit with Magento's built-in profiling, Blackfire, or New Relic, and you'll have the data to make confident decisions about which extensions earn their place on your store.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Magento 2 Profiling &amp; Performance Debugging: Find Bottlenecks Fast</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Fri, 29 May 2026 09:02:46 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-profiling-performance-debugging-find-bottlenecks-fast-1157</link>
      <guid>https://dev.to/magevanta/magento-2-profiling-performance-debugging-find-bottlenecks-fast-1157</guid>
      <description>&lt;p&gt;Performance issues in Magento 2 are rarely obvious. A page that takes 4 seconds might be suffering from a single misconfigured plugin, a runaway EAV query, or a third-party module making 200 redundant cache reads. Without proper profiling tools, you're flying blind — guessing, commenting out code, and hoping for the best.&lt;/p&gt;

&lt;p&gt;This guide walks through the full profiling toolkit for Magento 2: from quick built-in options to professional-grade APM tools like Blackfire and New Relic. You'll learn what to use, when to use it, and how to interpret the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Generic Profiling Isn't Enough
&lt;/h2&gt;

&lt;p&gt;Magento 2 is a complex, event-driven framework with hundreds of plugins, observers, and DI-compiled classes running on every request. A standard PHP profiler shows you call stacks, but doesn't give context about &lt;em&gt;why&lt;/em&gt; something is slow.&lt;/p&gt;

&lt;p&gt;You need tools that understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The plugin/interceptor chain&lt;/li&gt;
&lt;li&gt;DI container overhead&lt;/li&gt;
&lt;li&gt;MySQL query patterns (N+1, full scans)&lt;/li&gt;
&lt;li&gt;Cache hit/miss ratios&lt;/li&gt;
&lt;li&gt;Layout XML parsing and block rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's start simple and work up to the heavy artillery.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Magento's Built-in Profiler
&lt;/h2&gt;

&lt;p&gt;Magento ships with a lightweight built-in profiler you can enable without any extra tooling. It's ideal for quick debugging in local and staging environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enable via environment variable
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In your .htaccess or nginx config&lt;/span&gt;
&lt;span class="nv"&gt;MAGE_PROFILER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;html

&lt;span class="c"&gt;# Or via pub/index.php temporarily&lt;/span&gt;
&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'MAGE_PROFILER'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'html'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or set it in &lt;code&gt;app/etc/env.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'x-frame-options'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'SAMEORIGIN'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'MAGE_MODE'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'developer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'dev'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'profiler'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once enabled, a profiler table appears at the bottom of each page showing timer blocks, nesting levels, and call counts. Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Blocks with high "Avg" time and low call counts (expensive single operations)&lt;/li&gt;
&lt;li&gt;Blocks called hundreds of times (loop inefficiency or plugin waterfalls)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Custom profiler blocks in your own code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Magento\Framework\Profiler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Profiler&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my_custom_operation'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ... your code&lt;/span&gt;
&lt;span class="nc"&gt;Profiler&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my_custom_operation'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is great for benchmarking specific methods during development without a full APM setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. MySQL Query Logging
&lt;/h2&gt;

&lt;p&gt;Slow queries are the most common bottleneck in Magento. Enable MySQL slow query logging to capture them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- In MySQL session or my.cnf&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;slow_query_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ON'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;long_query_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;slow_query_log_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/var/log/mysql/slow.log'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;log_queries_not_using_indexes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ON'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then analyze with &lt;code&gt;mysqldumpslow&lt;/code&gt; or &lt;code&gt;pt-query-digest&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Group by query pattern, sort by total time&lt;/span&gt;
pt-query-digest /var/log/mysql/slow.log &lt;span class="nt"&gt;--limit&lt;/span&gt; 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Magento's query log via Varien_Db
&lt;/h3&gt;

&lt;p&gt;In developer mode, you can also log all queries Magento executes. Or use a custom plugin on &lt;code&gt;Magento\Framework\DB\Adapter\Pdo\Mysql::query()&lt;/code&gt; to log slow queries with their origin (class + method).&lt;/p&gt;

&lt;h3&gt;
  
  
  Spotting N+1 patterns
&lt;/h3&gt;

&lt;p&gt;The classic N+1 problem in Magento looks like this in your logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;43&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="k"&gt;more&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix: use &lt;code&gt;addAttributeToSelect('*')&lt;/code&gt; on collections and load them in bulk, or leverage the product repository with proper filtering.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Blackfire.io — The Gold Standard
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://blackfire.io" rel="noopener noreferrer"&gt;Blackfire&lt;/a&gt; is the professional choice for PHP profiling. It integrates natively with Magento 2 and produces call graphs that reveal exactly where time and memory are spent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install Blackfire PHP extension&lt;/span&gt;
curl &lt;span class="nt"&gt;-sS&lt;/span&gt; https://packages.blackfire.io/gpg.key | &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-key add -
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb http://packages.blackfire.io/debian any main"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/blackfire.list
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;blackfire-php blackfire

&lt;span class="c"&gt;# Configure credentials&lt;/span&gt;
blackfire agent:config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add to your &lt;code&gt;php.ini&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;blackfire.so&lt;/span&gt;
&lt;span class="py"&gt;blackfire.agent_socket&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;tcp://127.0.0.1:8307&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Profile a page
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;blackfire curl https://yourstore.local/catalog/category/view/id/5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use the browser extension for GUI-driven profiling with comparison between runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the call graph
&lt;/h3&gt;

&lt;p&gt;Blackfire generates an interactive flame graph. Key metrics to watch:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wall time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real elapsed time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Processing time (excludes I/O waits)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;I/O time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Disk/network waits (usually DB or cache)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Peak memory per call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Calls&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How many times a function was invoked&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Focus on nodes with high &lt;strong&gt;exclusive time&lt;/strong&gt; (time spent in that function itself, not its children). A function called 1,000 times with 0.1ms each adds up to 100ms — often invisible until you see the call count.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blackfire builds &amp;amp; assertions
&lt;/h3&gt;

&lt;p&gt;For CI/CD pipelines, define performance budgets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .blackfire.yaml&lt;/span&gt;
&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Homepage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fast"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.wall_time&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;800ms"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metrics.sql.queries.count&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;30"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metrics.cache.read.count&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;100"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fails the build if the homepage regresses beyond your defined thresholds.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. New Relic APM
&lt;/h2&gt;

&lt;p&gt;New Relic is better suited for production monitoring and long-term trend analysis. Where Blackfire is surgical (profile this specific request), New Relic is epidemiological (what's slow across thousands of requests).&lt;/p&gt;

&lt;h3&gt;
  
  
  Install the PHP agent
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-L&lt;/span&gt; https://download.newrelic.com/php_agent/release/newrelic-php5-&lt;span class="k"&gt;*&lt;/span&gt;.tar.gz | &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xz&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./newrelic-install &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure in &lt;code&gt;newrelic.ini&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;newrelic.appname&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Magento Production"&lt;/span&gt;
&lt;span class="py"&gt;newrelic.license&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR_LICENSE_KEY"&lt;/span&gt;
&lt;span class="py"&gt;newrelic.transaction_tracer.enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;newrelic.transaction_tracer.threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;500ms&lt;/span&gt;
&lt;span class="py"&gt;newrelic.slow_sql.enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What to watch in New Relic
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Transaction traces&lt;/strong&gt; — drill into the slowest requests over time. New Relic shows the full call stack including DB queries, external calls, and cache operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apdex score&lt;/strong&gt; — a customer satisfaction metric. Target &amp;gt; 0.9 for Magento stores. Anything below 0.7 needs immediate attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database tab&lt;/strong&gt; — shows the slowest SQL queries across all transactions, with execution plans and call frequency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom attributes&lt;/strong&gt; — add Magento-specific context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;extension_loaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'newrelic'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;newrelic_add_custom_parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'customer_group'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$customerGroup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;newrelic_add_custom_parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'store_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$storeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;newrelic_add_custom_parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cache_hit'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$cacheHit&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'yes'&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'no'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Tideways — APM Built for PHP Frameworks
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://tideways.com" rel="noopener noreferrer"&gt;Tideways&lt;/a&gt; is worth mentioning as a Magento-aware alternative to New Relic. It understands Magento's MVC structure and groups transactions by controller action automatically. Setup is similar to Blackfire — install the PHP extension, configure credentials, and it starts collecting data passively.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The Xdebug + KCachegrind Combo (Local Only)
&lt;/h2&gt;

&lt;p&gt;For deep local debugging, Xdebug's profiling mode generates Cachegrind files visualizable with KCachegrind or QCachegrind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; php.ini
&lt;/span&gt;&lt;span class="py"&gt;xdebug.mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;profile&lt;/span&gt;
&lt;span class="py"&gt;xdebug.output_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/tmp/xdebug&lt;/span&gt;
&lt;span class="py"&gt;xdebug.profiler_output_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;cachegrind.out.%t.%p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trigger with: &lt;code&gt;?XDEBUG_PROFILE=1&lt;/code&gt; on a request URL.&lt;/p&gt;

&lt;p&gt;Open the &lt;code&gt;.cachegrind&lt;/code&gt; file in KCachegrind to see a full call tree with inclusive/exclusive times.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Never enable Xdebug profiling in production — it adds massive overhead and generates large files.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Practical Debugging Workflow
&lt;/h2&gt;

&lt;p&gt;Here's a proven workflow when a Magento page is suddenly slow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check obvious causes first&lt;/strong&gt; — deployment recently? New module? Config cache cleared?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable MySQL slow query log&lt;/strong&gt; and check for expensive queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blackfire profile&lt;/strong&gt; the slow page — look at exclusive times &amp;gt; 50ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the plugin chain&lt;/strong&gt; on hot paths (e.g., &lt;code&gt;catalog_product_load_after&lt;/code&gt; observers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile again with the fix&lt;/strong&gt; — compare Blackfire runs side by side&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a Blackfire assertion&lt;/strong&gt; to prevent regression&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Quick wins to check first
&lt;/h3&gt;

&lt;p&gt;Before deep profiling, verify these common culprits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check OPcache is enabled and warm&lt;/span&gt;
php &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"var_dump(opcache_get_status()['opcache_enabled']);"&lt;/span&gt;

&lt;span class="c"&gt;# Check Redis connectivity and hit ratio&lt;/span&gt;
redis-cli INFO stats | &lt;span class="nb"&gt;grep &lt;/span&gt;keyspace

&lt;span class="c"&gt;# Check if Magento caches are enabled&lt;/span&gt;
bin/magento cache:status

&lt;span class="c"&gt;# Check for runaway cron jobs&lt;/span&gt;
bin/magento cron:status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Profiling in CI/CD
&lt;/h2&gt;

&lt;p&gt;Automate performance regression detection with Blackfire in your pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GitHub Actions example&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Blackfire performance tests&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;blackfire curl --json https://staging.yourstore.com/ \&lt;/span&gt;
      &lt;span class="s"&gt;--assert "main.wall_time &amp;lt; 1s"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents performance regressions from reaching production undetected — the same way unit tests prevent functional regressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Built-in Profiler&lt;/td&gt;
&lt;td&gt;Quick timer blocks&lt;/td&gt;
&lt;td&gt;Dev/staging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL slow log&lt;/td&gt;
&lt;td&gt;Query-level analysis&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blackfire&lt;/td&gt;
&lt;td&gt;Surgical code profiling&lt;/td&gt;
&lt;td&gt;Dev/staging/CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New Relic&lt;/td&gt;
&lt;td&gt;Production monitoring&lt;/td&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Xdebug + KCachegrind&lt;/td&gt;
&lt;td&gt;Deep call graph analysis&lt;/td&gt;
&lt;td&gt;Dev only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Profiling is a skill that compounds over time. The more you profile, the faster you'll spot patterns — that suspicious 300ms block in &lt;code&gt;Magento\Catalog\Model\ResourceModel\Product\Collection::load&lt;/code&gt;, the plugin interceptor chain that fires 40 times per request, or the layout XML file that parses 200KB of XML on every uncached page load.&lt;/p&gt;

&lt;p&gt;Start with the built-in profiler and MySQL logs. Graduate to Blackfire when you need precision. Use New Relic in production to catch what you missed. And always — &lt;em&gt;always&lt;/em&gt; — profile before and after any optimization to prove it actually helped.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Magento 2 Static Content Deploy Optimization: Faster Builds, Fewer Headaches</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Thu, 28 May 2026 09:02:32 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-static-content-deploy-optimization-faster-builds-fewer-headaches-1e9a</link>
      <guid>https://dev.to/magevanta/magento-2-static-content-deploy-optimization-faster-builds-fewer-headaches-1e9a</guid>
      <description>&lt;p&gt;Static content deployment is one of the most time-consuming steps in a Magento 2 release pipeline. On a large store with multiple themes, locales, and hundreds of modules, &lt;code&gt;setup:static-content:deploy&lt;/code&gt; can take anywhere from five minutes to over thirty. That's thirty minutes of downtime risk, blocked deployments, and frustrated developers staring at a progress bar.&lt;/p&gt;

&lt;p&gt;This guide covers every lever available to you: parallelization, scoped deploys, strategy selection, content versioning, and how to integrate all of this into a zero-downtime CI/CD pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Static Content Deploy Is Slow
&lt;/h2&gt;

&lt;p&gt;Before optimizing, understand the problem. When you run &lt;code&gt;bin/magento setup:static-content:deploy&lt;/code&gt;, Magento does the following for every theme/locale combination:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Resolves all Less/CSS source files, compiling them with the configured strategy&lt;/li&gt;
&lt;li&gt;Copies or symlinks JavaScript, images, and fonts from modules and themes&lt;/li&gt;
&lt;li&gt;Generates RequireJS configuration files&lt;/li&gt;
&lt;li&gt;Applies translations to JS files per locale&lt;/li&gt;
&lt;li&gt;Publishes everything to &lt;code&gt;pub/static/&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On a store with 3 themes × 5 locales, that's 15 combinations — each requiring full resolution of the entire asset tree. Add 200+ modules and you start to understand why it takes so long.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Parallelize With &lt;code&gt;-j&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The single most impactful flag is &lt;code&gt;-j&lt;/code&gt; (jobs), which controls parallelism:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento setup:static-content:deploy &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, Magento deploys each theme/locale combination sequentially. With &lt;code&gt;-j 4&lt;/code&gt;, it runs four processes in parallel. Set this to the number of available CPU cores — or slightly above for I/O-bound operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Detect CPU count and use it&lt;/span&gt;
&lt;span class="nv"&gt;CORES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;nproc &lt;/span&gt;2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; sysctl &lt;span class="nt"&gt;-n&lt;/span&gt; hw.ncpu 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;4&lt;span class="si"&gt;)&lt;/span&gt;
bin/magento setup:static-content:deploy &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; &lt;span class="nv"&gt;$CORES&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a 4-core server this alone can cut deploy time by 60-70%. On 8 cores, the gains compound further.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caution:&lt;/strong&gt; Don't blindly set &lt;code&gt;-j 16&lt;/code&gt; on a 4-core server. You'll swap memory and end up slower. Match cores to available resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Scope Your Deploy: Only What You Need
&lt;/h2&gt;

&lt;p&gt;Deploying every theme and locale is wasteful if your store only uses a subset. Scope it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento setup:static-content:deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-j&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--theme&lt;/span&gt; Magento/luma &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--theme&lt;/span&gt; Vendor/custom-theme &lt;span class="se"&gt;\&lt;/span&gt;
  nl_BE en_US fr_BE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass themes with &lt;code&gt;--theme&lt;/code&gt; and list locales as space-separated arguments at the end. This is dramatically faster than the default "everything" approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Define these in a deploy script variable so they're centrally maintained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;THEMES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--theme Vendor/custom-theme --theme Magento/backend"&lt;/span&gt;
&lt;span class="nv"&gt;LOCALES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"nl_BE en_US fr_BE de_DE"&lt;/span&gt;

bin/magento setup:static-content:deploy &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; 4 &lt;span class="nv"&gt;$THEMES&lt;/span&gt; &lt;span class="nv"&gt;$LOCALES&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never include &lt;code&gt;Magento/blank&lt;/code&gt; in production deploys unless a theme directly extends it without adding any customizations — even then, the admin theme covers most of its use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Choose the Right Deploy Strategy
&lt;/h2&gt;

&lt;p&gt;Magento offers three static content deploy strategies, configurable via &lt;code&gt;--strategy&lt;/code&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;quick&lt;/code&gt; (default since 2.3)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento setup:static-content:deploy &lt;span class="nt"&gt;--strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;quick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploys files once and symlinks duplicates. Fast, low disk usage. Best for most production stores.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;compact&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento setup:static-content:deploy &lt;span class="nt"&gt;--strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;compact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similar to quick but with a slightly different deduplication approach. Marginally slower but produces smaller output on disk. Use it when disk space is constrained.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;standard&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento setup:static-content:deploy &lt;span class="nt"&gt;--strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;standard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copies every file for every theme/locale combination without deduplication. Produces the largest output but is the most compatible. Only use this if you're seeing symlink-related issues with &lt;code&gt;quick&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For 99% of stores, &lt;code&gt;quick&lt;/code&gt; is the right choice. If you're still on the &lt;code&gt;standard&lt;/code&gt; default, switching to &lt;code&gt;quick&lt;/code&gt; alone can cut deploy time significantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Use &lt;code&gt;--no-html-minify&lt;/code&gt; During Development
&lt;/h2&gt;

&lt;p&gt;HTML minification of static templates adds time with minimal benefit in development contexts. Skip it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento setup:static-content:deploy &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--no-html-minify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production you want minification, but for staging pipelines where you're iterating on deploys, skipping it saves time.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Deploy Admin Separately
&lt;/h2&gt;

&lt;p&gt;The Magento admin (&lt;code&gt;Magento/backend&lt;/code&gt;) has its own asset tree. In a pipeline with a custom frontend theme, deploying admin and frontend in sequence wastes time. Structure it to run in parallel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run both in parallel background processes&lt;/span&gt;
bin/magento setup:static-content:deploy &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--theme&lt;/span&gt; Vendor/custom-theme nl_BE en_US fr_BE &amp;amp;

bin/magento setup:static-content:deploy &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--theme&lt;/span&gt; Magento/backend en_US &amp;amp;

&lt;span class="nb"&gt;wait
echo&lt;/span&gt; &lt;span class="s2"&gt;"Both deploys complete"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets your CI server use all cores across both operations simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Content Versioning Strategy
&lt;/h2&gt;

&lt;p&gt;Every static content deploy generates a new version ID, which busts the browser cache. This is good for cache invalidation but means users re-download all assets after every deploy — even if nothing changed.&lt;/p&gt;

&lt;p&gt;Check the current version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;pub/static/deployed_version.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For deployments where static assets haven't changed, you can preserve the existing version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Only redeploy if source files changed&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;git diff HEAD~1 &lt;span class="nt"&gt;--name-only&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'(view/|web/|layout/)'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;bin/magento setup:static-content:deploy &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; 4 &lt;span class="nv"&gt;$THEMES&lt;/span&gt; &lt;span class="nv"&gt;$LOCALES&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No static asset changes — skipping deploy"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is powerful in trunk-based development where most commits are backend-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Warm &lt;code&gt;pub/static&lt;/code&gt; Before Cutting Traffic
&lt;/h2&gt;

&lt;p&gt;Never cut traffic to a new release before static content is deployed. Structure your pipeline correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Deploy code&lt;/span&gt;
git pull origin main

&lt;span class="c"&gt;# 2. Run database upgrades (maintenance mode on)&lt;/span&gt;
bin/magento maintenance:enable
bin/magento setup:upgrade &lt;span class="nt"&gt;--keep-generated&lt;/span&gt;

&lt;span class="c"&gt;# 3. Deploy static content (maintenance still on)&lt;/span&gt;
bin/magento setup:static-content:deploy &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; 4 &lt;span class="nv"&gt;$THEMES&lt;/span&gt; &lt;span class="nv"&gt;$LOCALES&lt;/span&gt;

&lt;span class="c"&gt;# 4. Compile DI&lt;/span&gt;
bin/magento setup:di:compile

&lt;span class="c"&gt;# 5. Disable maintenance and flush cache&lt;/span&gt;
bin/magento maintenance:disable
bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;code&gt;setup:static-content:deploy&lt;/code&gt; writes to &lt;code&gt;pub/static/&lt;/code&gt; which is served directly by nginx. As soon as the files are there, they're live — even before maintenance mode is disabled. This means your CDN/Varnish can start warming the new assets while the site is still in maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Symlink Mode for Development
&lt;/h2&gt;

&lt;p&gt;On local development environments, deploying static content on every change is a workflow killer. Switch to developer mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento deploy:mode:set developer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In developer mode, Magento resolves static assets on-the-fly using symlinks into module &lt;code&gt;view/&lt;/code&gt; directories. No deploy step needed after every Less change. Use &lt;code&gt;grunt watch&lt;/code&gt; or the built-in Less compiler to compile CSS automatically.&lt;/p&gt;

&lt;p&gt;For a middle ground on staging — faster than full deploy but closer to production — use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento deploy:mode:set default
bin/magento setup:static-content:deploy &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--symlink-locale&lt;/span&gt; &lt;span class="nv"&gt;$THEMES&lt;/span&gt; &lt;span class="nv"&gt;$LOCALES&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. CI/CD Integration: Build Once, Distribute Many
&lt;/h2&gt;

&lt;p&gt;If your CI pipeline deploys to multiple identical servers (horizontal scaling), you don't need to run &lt;code&gt;setup:static-content:deploy&lt;/code&gt; on each one. Run it once, tar the output, and distribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On build server&lt;/span&gt;
bin/magento setup:static-content:deploy &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; 8 &lt;span class="nv"&gt;$THEMES&lt;/span&gt; &lt;span class="nv"&gt;$LOCALES&lt;/span&gt;
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-czf&lt;/span&gt; static-content-&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--short&lt;/span&gt; HEAD&lt;span class="si"&gt;)&lt;/span&gt;.tar.gz pub/static/

&lt;span class="c"&gt;# On each web server&lt;/span&gt;
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; static-content-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GIT_SHA&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensures all servers have identical static content (no race conditions)&lt;/li&gt;
&lt;li&gt;Cuts total deploy time proportional to the number of web servers&lt;/li&gt;
&lt;li&gt;Allows content to be pre-warmed before servers receive traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store the tarball in your artifact repository (S3, GitHub Packages, Artifactory) with the commit SHA as the identifier. If a rollback is needed, fetch the previous artifact rather than rerunning the deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Monitor Deploy Times
&lt;/h2&gt;

&lt;p&gt;Track &lt;code&gt;setup:static-content:deploy&lt;/code&gt; duration over time. A sudden spike usually signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A new module with a large &lt;code&gt;web/&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;A Less compilation error causing retries&lt;/li&gt;
&lt;li&gt;An accidental addition of an extra locale&lt;/li&gt;
&lt;li&gt;Disk I/O saturation on the build server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add timing to your deploy scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;START&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
bin/magento setup:static-content:deploy &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; 4 &lt;span class="nv"&gt;$THEMES&lt;/span&gt; &lt;span class="nv"&gt;$LOCALES&lt;/span&gt;
&lt;span class="nv"&gt;END&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Static deploy took &lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;END &lt;span class="o"&gt;-&lt;/span&gt; START&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;s"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feed these metrics into your monitoring system (Datadog, Grafana, or even a simple log file) so you can correlate deploy time growth with specific releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark: What to Expect
&lt;/h2&gt;

&lt;p&gt;Here's a rough guide for a mid-sized store (2 themes, 3 locales, ~200 modules):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Approximate Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default (sequential, standard strategy)&lt;/td&gt;
&lt;td&gt;18–25 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;-j 4&lt;/code&gt;, &lt;code&gt;quick&lt;/code&gt; strategy&lt;/td&gt;
&lt;td&gt;6–9 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;-j 8&lt;/code&gt;, scoped locales, &lt;code&gt;quick&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;3–5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distributed build (tarball)&lt;/td&gt;
&lt;td&gt;&amp;lt; 1 min per server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gains from parallelization and strategy selection are real and significant. There's no reason to accept a 20-minute deploy window when the same output can be generated in under five.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;Static content deployment doesn't have to be the bottleneck it is for most teams. The biggest wins:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;-j&lt;/code&gt;&lt;/strong&gt; with your core count — this alone is transformative&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope themes and locales&lt;/strong&gt; — don't deploy what you don't use&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;quick&lt;/code&gt; strategy&lt;/strong&gt; — it's the default for a reason, but many older stores still run &lt;code&gt;standard&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate admin from frontend&lt;/strong&gt; and run them in parallel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build once, distribute many&lt;/strong&gt; in horizontally-scaled environments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip redeploys&lt;/strong&gt; when static assets haven't changed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Apply these together and your 20-minute deploy window shrinks to under five minutes — leaving more time for shipping features instead of waiting on build pipelines.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>deployment</category>
    </item>
    <item>
      <title>Magento 2 Nginx Optimization for High Traffic — Complete Server Tuning Guide</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Wed, 27 May 2026 09:02:49 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-nginx-optimization-for-high-traffic-complete-server-tuning-guide-1g9f</link>
      <guid>https://dev.to/magevanta/magento-2-nginx-optimization-for-high-traffic-complete-server-tuning-guide-1g9f</guid>
      <description>&lt;p&gt;Your Magento store can have perfect PHP-FPM pools, well-tuned Redis, and optimized MySQL — and still buckle under load because Nginx is configured with defaults meant for a blog, not an ecommerce platform. Nginx is the front door to your entire stack. If it's slow, everything behind it is slow.&lt;/p&gt;

&lt;p&gt;This guide walks through every lever worth pulling: connection handling, compression, caching headers, microcaching, SSL/TLS, and real-world configs for a Magento 2 production server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Nginx Defaults Aren't Enough
&lt;/h2&gt;

&lt;p&gt;Out-of-the-box Nginx ships with conservative defaults:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;worker_processes 1&lt;/code&gt; — single worker, ignoring your CPU cores&lt;/li&gt;
&lt;li&gt;No gzip compression enabled&lt;/li&gt;
&lt;li&gt;Keepalive timeouts that force reconnects&lt;/li&gt;
&lt;li&gt;No browser cache headers&lt;/li&gt;
&lt;li&gt;TLS handshakes that repeat work they don't need to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a low-traffic site this doesn't matter. For Magento — which serves category pages, product pages, checkout flows, and API calls simultaneously — these defaults become bottlenecks at a few hundred concurrent users.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Worker Processes and Connections
&lt;/h2&gt;

&lt;p&gt;Start at the top of &lt;code&gt;nginx.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;worker_processes&lt;/span&gt; &lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;worker_rlimit_nofile&lt;/span&gt; &lt;span class="mi"&gt;65535&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;worker_connections&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="s"&gt;epoll&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;multi_accept&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;worker_processes auto&lt;/code&gt;&lt;/strong&gt; — Nginx will match the number of workers to available CPU cores. A 4-core server gets 4 workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;worker_rlimit_nofile 65535&lt;/code&gt;&lt;/strong&gt; — raises the OS file descriptor limit per worker. Each active connection uses a file descriptor; the default of 1024 is dangerously low for production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;worker_connections 4096&lt;/code&gt;&lt;/strong&gt; — maximum connections per worker. Total concurrent connections = &lt;code&gt;worker_processes × worker_connections&lt;/code&gt;. On a 4-core server: 16,384 concurrent connections max.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;use epoll&lt;/code&gt;&lt;/strong&gt; — Linux-only but highly efficient; scales better than &lt;code&gt;select&lt;/code&gt; or &lt;code&gt;poll&lt;/code&gt; under thousands of connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;multi_accept on&lt;/code&gt;&lt;/strong&gt; — workers accept all pending connections at once instead of one at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Gzip Compression
&lt;/h2&gt;

&lt;p&gt;Magento pages are large: HTML pages often exceed 100KB before JS and CSS. Gzip cuts transfer sizes by 60–80%:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;gzip&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;gzip_vary&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;gzip_proxied&lt;/span&gt; &lt;span class="s"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;gzip_comp_level&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;gzip_min_length&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;gzip_types&lt;/span&gt;
        &lt;span class="nc"&gt;text/plain&lt;/span&gt;
        &lt;span class="nc"&gt;text/css&lt;/span&gt;
        &lt;span class="nc"&gt;text/xml&lt;/span&gt;
        &lt;span class="nc"&gt;text/javascript&lt;/span&gt;
        &lt;span class="nc"&gt;application/javascript&lt;/span&gt;
        &lt;span class="nc"&gt;application/x-javascript&lt;/span&gt;
        &lt;span class="nc"&gt;application/json&lt;/span&gt;
        &lt;span class="nc"&gt;application/xml&lt;/span&gt;
        &lt;span class="nc"&gt;application/xml&lt;/span&gt;&lt;span class="s"&gt;+rss&lt;/span&gt;
        &lt;span class="nc"&gt;application/vnd&lt;/span&gt;&lt;span class="s"&gt;.ms-fontobject&lt;/span&gt;
        &lt;span class="nc"&gt;font/eot&lt;/span&gt;
        &lt;span class="nc"&gt;font/otf&lt;/span&gt;
        &lt;span class="nc"&gt;font/ttf&lt;/span&gt;
        &lt;span class="nc"&gt;image/svg&lt;/span&gt;&lt;span class="s"&gt;+xml&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;gzip_comp_level 5&lt;/code&gt;&lt;/strong&gt; — the sweet spot. Level 9 uses ~30% more CPU for only ~2% better compression. Level 5 is fast and effective.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gzip_vary on&lt;/code&gt;&lt;/strong&gt; — adds a &lt;code&gt;Vary: Accept-Encoding&lt;/code&gt; header so CDNs and proxies serve the right version to each client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gzip_min_length 1000&lt;/code&gt;&lt;/strong&gt; — don't bother compressing tiny responses; overhead outweighs benefit below ~1KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Keepalive and Timeouts
&lt;/h2&gt;

&lt;p&gt;Keepalive connections let the browser reuse TCP connections for multiple requests, eliminating repeated TCP handshakes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;keepalive_timeout&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;keepalive_requests&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Upstream keepalive to PHP-FPM&lt;/span&gt;
    &lt;span class="kn"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;fastcgi_backend&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="s"&gt;unix:/run/php/php8.3-fpm.sock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;keepalive&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;keepalive_timeout 65&lt;/code&gt;&lt;/strong&gt; — hold connections open for 65 seconds. Fine for most workloads; reduce to 15–30 on memory-constrained servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;keepalive 32&lt;/code&gt;&lt;/strong&gt; in the upstream block — keeps up to 32 persistent connections open to PHP-FPM, avoiding socket overhead on every PHP request.&lt;/p&gt;

&lt;p&gt;Also tune these to avoid slow clients tying up workers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;client_header_timeout&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;client_body_timeout&lt;/span&gt;  &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;send_timeout&lt;/span&gt;         &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;reset_timedout_connection&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Browser Cache Headers for Static Assets
&lt;/h2&gt;

&lt;p&gt;Magento's static files (JS, CSS, images, fonts) are versioned via deploy version hashes. Set aggressive caching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;1y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"public,&lt;/span&gt; &lt;span class="s"&gt;immutable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;immutable&lt;/code&gt;&lt;/strong&gt; tells supporting browsers (Chrome, Firefox) to never re-validate the file during its lifetime — eliminating conditional GET requests entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;access_log off&lt;/code&gt;&lt;/strong&gt; for static assets reduces disk I/O significantly on busy servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Microcaching with FastCGI Cache
&lt;/h2&gt;

&lt;p&gt;Full Page Cache (Varnish or Magento built-in) handles authenticated sessions poorly by design. Microcaching fills the gap: cache PHP responses for just 1–5 seconds. At 500 req/s, a 1-second cache reduces PHP hits by ~99% for repeated URLs.&lt;/p&gt;

&lt;p&gt;Define a cache zone in &lt;code&gt;nginx.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;fastcgi_cache_path&lt;/span&gt; &lt;span class="n"&gt;/var/cache/nginx&lt;/span&gt; &lt;span class="s"&gt;levels=1:2&lt;/span&gt; &lt;span class="s"&gt;keys_zone=MAGENTO:100m&lt;/span&gt; &lt;span class="s"&gt;inactive=60m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;fastcgi_cache_key&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$scheme$request_method$host$request_uri&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your server block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;$no_cache&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Don't cache POST requests&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;POST)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;$no_cache&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Don't cache if session cookie present (logged-in users, active cart)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$http_cookie&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;"(PHPSESSID|frontend|adminhtml|checkout)")&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;$no_cache&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.php$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_cache&lt;/span&gt; &lt;span class="s"&gt;MAGENTO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_cache_valid&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="s"&gt;1s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_cache_bypass&lt;/span&gt; &lt;span class="nv"&gt;$no_cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_no_cache&lt;/span&gt; &lt;span class="nv"&gt;$no_cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-FastCGI-Cache&lt;/span&gt; &lt;span class="nv"&gt;$upstream_cache_status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;fastcgi_params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_pass&lt;/span&gt; &lt;span class="s"&gt;fastcgi_backend&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_param&lt;/span&gt; &lt;span class="s"&gt;SCRIPT_FILENAME&lt;/span&gt; &lt;span class="nv"&gt;$document_root$fastcgi_script_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;X-FastCGI-Cache&lt;/code&gt; header lets you verify HIT/MISS/BYPASS in response headers — essential for debugging. If you see BYPASS on every request, check that your cookie exclusions are correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Do not use microcaching in place of proper FPC. Use it as a complement for high-burst traffic windows (flash sales, launches).&lt;/p&gt;

&lt;h2&gt;
  
  
  6. SSL/TLS Performance
&lt;/h2&gt;

&lt;p&gt;TLS termination at Nginx is unavoidable on HTTPS-only stores. Tune it to minimize handshake overhead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;ssl_protocols&lt;/span&gt; &lt;span class="s"&gt;TLSv1.2&lt;/span&gt; &lt;span class="s"&gt;TLSv1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_ciphers&lt;/span&gt; &lt;span class="s"&gt;'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_prefer_server_ciphers&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ssl_session_cache&lt;/span&gt; &lt;span class="s"&gt;shared:SSL:10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_session_timeout&lt;/span&gt; &lt;span class="mi"&gt;10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_session_tickets&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;# Disable for perfect forward secrecy&lt;/span&gt;

&lt;span class="k"&gt;ssl_stapling&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_stapling_verify&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;resolver&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="s"&gt;.1.1&lt;/span&gt; &lt;span class="mf"&gt;8.8&lt;/span&gt;&lt;span class="s"&gt;.8.8&lt;/span&gt; &lt;span class="s"&gt;valid=300s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;resolver_timeout&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;ssl_session_cache shared:SSL:10m&lt;/code&gt;&lt;/strong&gt; — a 10MB shared cache holds ~40,000 sessions, allowing TLS resumption (no full handshake for returning visitors).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OCSP stapling&lt;/strong&gt; (&lt;code&gt;ssl_stapling on&lt;/code&gt;) — Nginx fetches and caches the certificate validity check, so your clients don't have to. Eliminates one round-trip per new connection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLSv1.3&lt;/strong&gt; — if you can drop TLSv1.2 entirely (verify your CDN and payment providers support it), TLSv1.3 does a full handshake in 1 round trip vs. 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Buffer Tuning
&lt;/h2&gt;

&lt;p&gt;Large Magento responses (admin grids, product list pages) benefit from proper buffer settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;client_body_buffer_size&lt;/span&gt; &lt;span class="mi"&gt;128k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;client_max_body_size&lt;/span&gt;    &lt;span class="mi"&gt;64m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# Required for media imports&lt;/span&gt;

&lt;span class="k"&gt;proxy_buffer_size&lt;/span&gt;          &lt;span class="mi"&gt;4k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_buffers&lt;/span&gt;            &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="mi"&gt;32k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_busy_buffers_size&lt;/span&gt;  &lt;span class="mi"&gt;64k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fastcgi_buffers&lt;/span&gt;          &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="mi"&gt;16k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;fastcgi_buffer_size&lt;/span&gt;      &lt;span class="mi"&gt;32k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;fastcgi_buffers 16 16k&lt;/code&gt;&lt;/strong&gt; — 256KB total buffer per request. Enough for most Magento pages without disk buffering.&lt;/p&gt;

&lt;p&gt;If a page exceeds &lt;code&gt;fastcgi_buffers&lt;/code&gt;, Nginx writes the overflow to a temp file — adding disk I/O to every large response. Set this high enough to avoid it.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Rate Limiting for Admin and API
&lt;/h2&gt;

&lt;p&gt;Protect your admin panel and REST API from brute-force and abuse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Define zones in http block&lt;/span&gt;
&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=admin:10m&lt;/span&gt; &lt;span class="s"&gt;rate=5r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=api:10m&lt;/span&gt; &lt;span class="s"&gt;rate=30r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Apply in server block&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/admin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=admin&lt;/span&gt; &lt;span class="s"&gt;burst=10&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# ... rest of config&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/rest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=api&lt;/span&gt; &lt;span class="s"&gt;burst=50&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# ... rest of config&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;burst&lt;/code&gt;&lt;/strong&gt; allows short spikes above the rate. &lt;strong&gt;&lt;code&gt;nodelay&lt;/code&gt;&lt;/strong&gt; processes burst requests immediately instead of queuing them — important for API clients that batch requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Monitoring What You've Done
&lt;/h2&gt;

&lt;p&gt;After applying changes, verify with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Test config before reloading&lt;/span&gt;
nginx &lt;span class="nt"&gt;-t&lt;/span&gt;

&lt;span class="c"&gt;# Reload without dropping connections&lt;/span&gt;
nginx &lt;span class="nt"&gt;-s&lt;/span&gt; reload

&lt;span class="c"&gt;# Watch real-time connection states&lt;/span&gt;
ss &lt;span class="nt"&gt;-s&lt;/span&gt;

&lt;span class="c"&gt;# Check cache hit rate (tail access log with cache status)&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/nginx/access.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'X-Cache'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;ab&lt;/code&gt; (ApacheBench) or &lt;code&gt;wrk&lt;/code&gt; for quick load tests before and after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1000 requests, 50 concurrent&lt;/span&gt;
ab &lt;span class="nt"&gt;-n&lt;/span&gt; 1000 &lt;span class="nt"&gt;-c&lt;/span&gt; 50 https://your-store.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Nginx optimization is one of the highest-leverage things you can do for a Magento store: it affects every single request before PHP even runs. The changes above — workers, gzip, keepalive, browser cache, microcaching, TLS tuning, and buffers — consistently yield 2–4× improvement in requests-per-second capacity on the same hardware.&lt;/p&gt;

&lt;p&gt;Start with &lt;code&gt;worker_processes auto&lt;/code&gt; and gzip (both zero-risk changes), then profile with a load test before adding microcaching, which requires careful cookie exclusion to avoid caching user-specific content.&lt;/p&gt;

&lt;p&gt;The full picture: Nginx handles connections and serves static files; Varnish or Magento FPC serves full pages; Redis caches sessions and blocks; PHP-FPM processes what's left. Each layer does its job. Nginx's job is to be fast and efficient at the very edge — don't let defaults undermine the rest of your stack.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>nginx</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Magento 2 B2B Performance Optimization: Shared Catalogs, Quotes &amp; Company Accounts</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Tue, 26 May 2026 09:03:16 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-b2b-performance-optimization-shared-catalogs-quotes-company-accounts-453m</link>
      <guid>https://dev.to/magevanta/magento-2-b2b-performance-optimization-shared-catalogs-quotes-company-accounts-453m</guid>
      <description>&lt;p&gt;Adobe Commerce B2B unlocks powerful functionality for wholesale and enterprise merchants — company accounts, shared catalogs, negotiable quotes, quick order, and requisition lists. But these features come with a significant performance cost if left unconfigured. In this post we dig into the most common B2B bottlenecks and how to systematically address each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why B2B Is Different from B2C
&lt;/h2&gt;

&lt;p&gt;Standard Magento 2 performance advice — Redis caching, Varnish, OPcache, flat tables — still applies to B2B. But B2B merchants face a set of additional challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shared catalogs&lt;/strong&gt; create per-company product visibility rules that bypass the standard catalog cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negotiable quotes&lt;/strong&gt; generate large quote objects with complex price recalculations on every update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Company hierarchies&lt;/strong&gt; add permission checks to nearly every storefront request for logged-in users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requisition lists&lt;/strong&gt; issue repeated save/load queries against the &lt;code&gt;negotiable_quote&lt;/code&gt; and &lt;code&gt;requisition_list&lt;/code&gt; tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: B2B storefronts often run 2–4× slower than equivalent B2C stores with no additional configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Shared Catalog — The Hidden Query Bomb
&lt;/h2&gt;

&lt;p&gt;Shared catalogs work by filtering product visibility per company via the &lt;code&gt;shared_catalog_product_item&lt;/code&gt; table. Each catalog page load for a logged-in B2B customer hits this table with large JOIN queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to profile
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;EXPLAIN&lt;/code&gt; on a category page query with shared catalogs enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;shared_catalog_product_item&lt;/span&gt; &lt;span class="n"&gt;sci&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;sci&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;sci&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_group_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Missing indexes on &lt;code&gt;(sku, customer_group_id)&lt;/code&gt; are the most common culprit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Add a composite index&lt;/strong&gt; if it's missing (check your version — Adobe Commerce 2.4.5+ has this):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;shared_catalog_product_item&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IDX_SKU_GROUP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_group_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Enable flat catalog&lt;/strong&gt; for B2B stores with large catalogs (&amp;gt;50k SKUs). Despite being deprecated, flat tables still dramatically reduce EAV JOINs on category pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento config:set catalog/frontend/flat_catalog_product 1
bin/magento config:set catalog/frontend/flat_catalog_category 1
bin/magento indexer:reindex catalog_product_flat catalog_category_flat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Limit shared catalog depth.&lt;/strong&gt; Every additional custom catalog multiplies reindex time and query complexity. Audit whether all catalogs are actually in use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_group_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;shared_catalog&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete or merge unused catalogs via the Admin panel before they compound the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Negotiable Quotes — Price Recalculation Is Expensive
&lt;/h2&gt;

&lt;p&gt;Negotiable quotes are notorious for triggering full cart recalculations. Every time a sales rep adjusts a line item, Magento recalculates the entire quote: shipping estimates, tax, discounts, custom prices. On quotes with 200+ line items this can take 10–30 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reduce recalculation triggers
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;app/etc/config.php&lt;/code&gt; or via Admin, limit quote recalculation to explicit submit actions rather than on every save:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stores → Configuration → B2B Features → Default B2B Payment Methods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More importantly, &lt;strong&gt;disable automatic quote totals recalculation on every admin edit&lt;/strong&gt;. Patch the &lt;code&gt;Magento\NegotiableQuote\Model\Quote\Totals&lt;/code&gt; class via a plugin to defer recalculation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Plugin: defer recalculation to explicit recalculate() call only&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;aroundRecalculateQuote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;\Magento\NegotiableQuote\Model\Quote\Totals&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;callable&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$force&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$force&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$force&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Skip automatic recalculation triggered by save events&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply carefully and test pricing accuracy thoroughly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Index and archive old quotes
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;quote&lt;/code&gt; and &lt;code&gt;negotiable_quote&lt;/code&gt; tables grow unbounded on active B2B stores. Queries slow down as these tables exceed millions of rows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Find quotes older than 6 months that are closed/declined&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;negotiable_quote&lt;/span&gt; &lt;span class="n"&gt;nq&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quote_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;nq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'closed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'declined'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="k"&gt;MONTH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Archive or purge stale quotes on a regular schedule using a custom cron job. Adobe Commerce does not do this automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Company Account Permission Checks
&lt;/h2&gt;

&lt;p&gt;Every request from a logged-in B2B user triggers a company permission check via &lt;code&gt;Magento\Company\Model\Authorization&lt;/code&gt;. On storefronts with complex role hierarchies (company → division → user), this becomes expensive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache company permissions
&lt;/h3&gt;

&lt;p&gt;Company permission data changes rarely. Add a Redis-backed cache layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In your around plugin on CompanyManagement::getByCustomerId()&lt;/span&gt;
&lt;span class="nv"&gt;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'company_user_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$customerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cached&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$customerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'company_permissions'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="mi"&gt;3600&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Invalidate the tag &lt;code&gt;company_permissions&lt;/code&gt; on company/role changes via an observer on &lt;code&gt;company_save_after&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimize role tree depth
&lt;/h3&gt;

&lt;p&gt;Deeply nested company structures (5+ levels) multiply permission checks. Encourage merchants to keep hierarchies flat: company → department → user (3 levels maximum). Beyond that, query count grows exponentially.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Requisition Lists — Silent Scalability Problem
&lt;/h2&gt;

&lt;p&gt;Requisition lists seem lightweight but they issue a database read on every storefront visit for logged-in B2B users. Customers with 20+ requisition lists of 100+ items each generate dozens of queries per page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lazy-load requisition list counts
&lt;/h3&gt;

&lt;p&gt;The storefront header displays a requisition list count. By default this is loaded synchronously. Move it to a separate AJAX call that runs after page load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- layout/default.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;block&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"Magento\RequisitionList\Block\RequisitionList\Counter"&lt;/span&gt;
       &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"requisition.list.counter"&lt;/span&gt;
       &lt;span class="na"&gt;template=&lt;/span&gt;&lt;span class="s"&gt;"Magento_RequisitionList::counter.phtml"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;arguments&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;argument&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"lazy_load"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"boolean"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/argument&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/arguments&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/block&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Implement the lazy-load controller endpoint at &lt;code&gt;requisition_list/ajax/count&lt;/code&gt; and update the template to defer via &lt;code&gt;fetch()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paginate large requisition lists
&lt;/h3&gt;

&lt;p&gt;A single requisition list with 500 items forces Magento to load all items before rendering. Add server-side pagination in your custom module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$collection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;requisitionListItemRepository&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$searchCriteria&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setPageSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setCurrentPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This alone can reduce TTFB on the requisition list page from 4s to under 800ms for large lists.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. B2B Indexer Performance
&lt;/h2&gt;

&lt;p&gt;B2B adds several indexers that run on top of the standard Magento indexers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;shared_catalog_product_price&lt;/code&gt; — runs on every shared catalog change&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;b2b_company_structure&lt;/code&gt; — rebuilds company trees&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;negotiable_quote_grid&lt;/code&gt; — refreshes the admin quote grid&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These indexers are not always set to &lt;code&gt;Update on Schedule&lt;/code&gt; out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Force schedule mode for all B2B indexers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento indexer:set-mode schedule shared_catalog_product_price
bin/magento indexer:set-mode schedule b2b_company_structure
bin/magento indexer:set-mode schedule negotiable_quote_grid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento indexer:status | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"shared_catalog|b2b_company|negotiable"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All B2B indexers should show &lt;code&gt;Update by Schedule&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. HTTP Cache Strategy for B2B
&lt;/h2&gt;

&lt;p&gt;Full-page caching is effectively disabled for logged-in B2B customers — they see personalized catalogs, prices, and credit limits. This means your origin servers absorb every B2B request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session-less catalog pages
&lt;/h3&gt;

&lt;p&gt;For large B2B stores, build a "guest view" of catalog pages that is FPC-cacheable, with personalized data (price, availability) loaded via AJAX after page paint. This is the same technique used in B2C "hole punching" but applied more aggressively.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP/2 push for critical B2B assets
&lt;/h3&gt;

&lt;p&gt;B2B pages load more JavaScript (quote widget, company menu, requisition controls). Configure Nginx or your CDN to HTTP/2 push core B2B JS bundles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/b2b-account&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;http2_push&lt;/span&gt; &lt;span class="n"&gt;/pub/static/frontend/Magento/luma/en_US/Magento_NegotiableQuote/js/quote.min.js&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;http2_push&lt;/span&gt; &lt;span class="n"&gt;/pub/static/frontend/Magento/luma/en_US/Magento_Company/js/company.min.js&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Monitoring B2B Performance
&lt;/h2&gt;

&lt;p&gt;Standard APM tools miss B2B-specific slow paths. Add custom instrumentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In a plugin on NegotiableQuoteManagement::send()&lt;/span&gt;
&lt;span class="nv"&gt;$startTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$startTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Slow negotiable quote send'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'quote_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$quoteId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'elapsed_ms'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$elapsed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Track and alert on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Negotiable quote recalculation time &amp;gt; 3s&lt;/li&gt;
&lt;li&gt;Shared catalog reindex duration &amp;gt; 5 min&lt;/li&gt;
&lt;li&gt;Requisition list page TTFB &amp;gt; 1s&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary: B2B Performance Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shared catalog&lt;/td&gt;
&lt;td&gt;Add composite index on &lt;code&gt;(sku, customer_group_id)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared catalog&lt;/td&gt;
&lt;td&gt;Archive unused catalogs&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Negotiable quotes&lt;/td&gt;
&lt;td&gt;Defer auto-recalculation&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Negotiable quotes&lt;/td&gt;
&lt;td&gt;Archive closed/declined quotes&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Company permissions&lt;/td&gt;
&lt;td&gt;Cache per-user permission data in Redis&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Company structure&lt;/td&gt;
&lt;td&gt;Keep hierarchy ≤ 3 levels deep&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requisition lists&lt;/td&gt;
&lt;td&gt;Lazy-load header counter&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requisition lists&lt;/td&gt;
&lt;td&gt;Paginate large lists&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2B indexers&lt;/td&gt;
&lt;td&gt;Set all to &lt;code&gt;Update by Schedule&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP caching&lt;/td&gt;
&lt;td&gt;AJAX-load personalized data, cache catalog shell&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;B2B performance is not a single-fix problem — it requires work across the stack. Start with indexer modes and the shared catalog index, as those yield the highest return with the lowest risk. Then work through quote archiving and company permission caching. With these changes in place, most B2B storefronts can achieve sub-second category pages even under authenticated load.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Cache Warming Strategies: Keep Your Store Fast After Every Deploy</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 24 May 2026 09:02:09 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-cache-warming-strategies-keep-your-store-fast-after-every-deploy-3f59</link>
      <guid>https://dev.to/magevanta/magento-2-cache-warming-strategies-keep-your-store-fast-after-every-deploy-3f59</guid>
      <description>&lt;p&gt;You've spent weeks tuning your Magento 2 Full Page Cache. Varnish is configured, FPC hit rates are above 90%, and your TTFB is under 200ms. Then you deploy a hotfix at 2 AM and flush the cache. Suddenly, every customer who visits gets a cold, uncached response — PHP bootstrapping, layout building, block rendering — the full stack, for every single request.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;cold cache problem&lt;/strong&gt;, and it's one of the most overlooked performance gaps in Magento 2 operations. Cache warming is the solution: proactively generating cached responses before real users arrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cache Warming Matters
&lt;/h2&gt;

&lt;p&gt;Magento 2's Full Page Cache is lazy by default. A page only gets cached after the &lt;em&gt;first&lt;/em&gt; request hits the backend. On large stores with thousands of product, category, and CMS pages, this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every deploy causes a &lt;strong&gt;performance cliff&lt;/strong&gt; until traffic naturally re-warms the cache&lt;/li&gt;
&lt;li&gt;Cache flushes during low-traffic windows (maintenance, reindexing) leave mornings slow&lt;/li&gt;
&lt;li&gt;Bots and crawlers that hit un-cached pages waste server resources on expensive renders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good warming strategy fills the cache &lt;em&gt;before&lt;/em&gt; users notice, turning the cold-start problem into a non-event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 1: Sitemap-Based Crawling
&lt;/h2&gt;

&lt;p&gt;The simplest and most maintainable approach. Magento generates a sitemap at &lt;code&gt;pub/sitemap.xml&lt;/code&gt; (or a sitemap index). Use this as your source of truth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# warm-cache.sh — crawl all URLs from sitemap&lt;/span&gt;

&lt;span class="nv"&gt;SITEMAP_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://your-store.com/sitemap.xml"&lt;/span&gt;
&lt;span class="nv"&gt;CONCURRENCY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4
&lt;span class="nv"&gt;USER_AGENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"MagevantaCacheWarmer/1.0"&lt;/span&gt;

&lt;span class="c"&gt;# Extract URLs from sitemap and crawl with wget&lt;/span&gt;
wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; - &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SITEMAP_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'(?&amp;lt;=&amp;lt;loc&amp;gt;)[^&amp;lt;]+'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | xargs &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONCURRENCY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_AGENT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Forwarded-Proto: https"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--max-time&lt;/span&gt; 10 &lt;span class="s2"&gt;"{}"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Cache warm complete."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;CONCURRENCY&lt;/code&gt; based on your server capacity. For most stores, 4–8 parallel requests is safe without overwhelming the backend. Run this script immediately after every deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; If you have a sitemap index (&lt;code&gt;&amp;lt;sitemapindex&amp;gt;&lt;/code&gt;), parse the child sitemaps first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; - &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SITEMAP_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'(?&amp;lt;=&amp;lt;loc&amp;gt;)[^&amp;lt;]+\.xml'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | xargs &lt;span class="nt"&gt;-I&lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt; wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; - &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'(?&amp;lt;=&amp;lt;loc&amp;gt;)[^&amp;lt;]+'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | xargs &lt;span class="nt"&gt;-P&lt;/span&gt; 4 &lt;span class="nt"&gt;-I&lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt; curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="s2"&gt;"{}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Strategy 2: Magerun2 Integration
&lt;/h2&gt;

&lt;p&gt;If you use &lt;code&gt;n98-magerun2&lt;/code&gt; in your workflow, the &lt;code&gt;sys:url:list&lt;/code&gt; command gives you all store URLs programmatically — respecting store views, locales, and URL configurations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate all URLs from Magerun and warm them&lt;/span&gt;
php /usr/local/bin/n98-magerun2 sys:url:list &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--add-all-stores&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;plain &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^http'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | xargs &lt;span class="nt"&gt;-P&lt;/span&gt; 6 &lt;span class="nt"&gt;-I&lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt; curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Forwarded-Proto: https"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"CacheWarmer/1.0"&lt;/span&gt; &lt;span class="s2"&gt;"{}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is particularly useful for &lt;strong&gt;multistore setups&lt;/strong&gt; where you have multiple base URLs that may not all appear in a single sitemap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 3: Prioritized Warming
&lt;/h2&gt;

&lt;p&gt;Not all pages are equal. Your homepage, top category pages, and bestseller PDPs get 10x more traffic than the long tail. Warm high-priority pages first.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;warm-priority.txt&lt;/code&gt; with your most important URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-store.com/
https://your-store.com/sale.html
https://your-store.com/new-arrivals.html
https://your-store.com/men.html
https://your-store.com/women.html
https://your-store.com/brands.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your deploy pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Phase 1: warm critical pages immediately (sequential for reliability)&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; url&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; warm-priority.txt

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Priority pages warmed. Starting full crawl in background..."&lt;/span&gt;

&lt;span class="c"&gt;# Phase 2: warm the rest asynchronously&lt;/span&gt;
&lt;span class="nb"&gt;nohup&lt;/span&gt; ./warm-cache.sh &amp;amp;&amp;gt;/var/log/cache-warm.log &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures the most important pages are cached within seconds of deploy, while the full warm runs in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 4: Varnish PURGE + Warm Pipeline
&lt;/h2&gt;

&lt;p&gt;If you're running Varnish in front of Magento, you can build a smarter pipeline: instead of warming &lt;em&gt;everything&lt;/em&gt; after a flush, only warm pages that were actually purged.&lt;/p&gt;

&lt;p&gt;Magento sends &lt;code&gt;PURGE&lt;/code&gt; requests to Varnish when cache tags are invalidated (e.g., after saving a product). You can intercept these in Varnish using a custom VCL logging hook, then re-crawl only those URLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vcl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub&lt;/span&gt; &lt;span class="nf"&gt;vcl_recv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"PURGE"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# Log purged URLs for the re-warmer&lt;/span&gt;
    &lt;span class="nf"&gt;std.log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"PURGE:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;req.url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parse the Varnish log with &lt;code&gt;varnishlog&lt;/code&gt; and feed purged URLs to your crawler. This is advanced but dramatically reduces warming time on large stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 5: Magento 2 Cron-Based Warming
&lt;/h2&gt;

&lt;p&gt;For stores that flush cache during maintenance windows (e.g., after a reindex), add a Magento cron job that triggers warming automatically.&lt;/p&gt;

&lt;p&gt;Create a simple cron class in your module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Vendor\CacheWarmer\Cron&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Magento\Framework\HTTP\Client\Curl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WarmCache&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;Curl&lt;/span&gt; &lt;span class="nv"&gt;$curl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;CollectionFactory&lt;/span&gt; &lt;span class="nv"&gt;$sitemapCollectionFactory&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$sitemaps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sitemapCollectionFactory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sitemaps&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$sitemap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;warmFromSitemap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sitemap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSitemapUrl&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;warmFromSitemap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;curl&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;curl&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getBody&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nb"&gt;preg_match_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/&amp;lt;loc&amp;gt;([^&amp;lt;]+)&amp;lt;\/loc&amp;gt;/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$matches&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$pageUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;curl&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pageUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register this in &lt;code&gt;crontab.xml&lt;/code&gt; to run after peak cache-clearing operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warming with Customer Context
&lt;/h2&gt;

&lt;p&gt;One subtlety: Magento FPC can vary by customer group (not logged in, logged in, specific groups). By default, only the &lt;code&gt;CUSTOMER_GROUP_ID=0&lt;/code&gt; (guest) variant is cached via FPC. Your warmer should simulate guest requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Forwarded-Proto: https"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept-Encoding: gzip"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid sending session cookies in warming requests — this will generate separate cache variations per session and negate the warming benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy Pipeline Integration
&lt;/h2&gt;

&lt;p&gt;The ideal warming workflow integrates directly into your CI/CD pipeline. Using GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Warm Magento Cache&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;echo "Waiting for Varnish to propagate purges..."&lt;/span&gt;
    &lt;span class="s"&gt;sleep 10&lt;/span&gt;
    &lt;span class="s"&gt;chmod +x ./scripts/warm-cache.sh&lt;/span&gt;
    &lt;span class="s"&gt;./scripts/warm-cache.sh&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;STORE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.STORE_URL }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this step &lt;em&gt;after&lt;/em&gt; &lt;code&gt;php bin/magento cache:flush&lt;/code&gt; and &lt;em&gt;before&lt;/em&gt; removing the maintenance flag. Users won't be let in until the critical pages are already warm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring Warming Effectiveness
&lt;/h2&gt;

&lt;p&gt;Track your warming success with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check FPC hit rate in Magento logs&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"cache_type.*full_page"&lt;/span&gt; var/log/system.log | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt;

&lt;span class="c"&gt;# Check Varnish hit rate&lt;/span&gt;
varnishstat &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; MAIN.cache_hit &lt;span class="nt"&gt;-f&lt;/span&gt; MAIN.cache_miss &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1, $2}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;paste&lt;/span&gt; - - &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{printf "Hit rate: %.1f%%\n", $2/($2+$4)*100}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A well-warmed cache should hit 80–90% within 5 minutes of a deploy on a typical store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always warm after deploy&lt;/strong&gt; — don't let users hit the cold cache&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prioritize high-traffic pages&lt;/strong&gt; — warm them first, synchronously&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use your sitemap&lt;/strong&gt; — it's the simplest accurate source of pages to warm&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set concurrency carefully&lt;/strong&gt; — too many parallel requests can spike your backend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrate into CI/CD&lt;/strong&gt; — make warming automatic, not manual&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cache warming is a small operational investment with a disproportionate impact on perceived performance. Your store should feel equally fast at 2:01 AM post-deploy as it does at peak traffic — and with a solid warming strategy, it will.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>caching</category>
    </item>
    <item>
      <title>Magento 2 Layered Navigation Performance: Diagnose &amp; Fix Slow Category Pages</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sat, 23 May 2026 09:02:29 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-layered-navigation-performance-diagnose-fix-slow-category-pages-564j</link>
      <guid>https://dev.to/magevanta/magento-2-layered-navigation-performance-diagnose-fix-slow-category-pages-564j</guid>
      <description>&lt;p&gt;Category pages are often the first thing visitors interact with — and the first thing that tanks under load. If your layered navigation takes 3+ seconds to render, you're losing conversions before a single product is clicked. This post breaks down &lt;em&gt;why&lt;/em&gt; Magento 2 layered navigation gets slow and gives you the tools to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Layered Navigation Works Under the Hood
&lt;/h2&gt;

&lt;p&gt;Before diagnosing, you need to understand what Magento is actually doing when it renders those filter options.&lt;/p&gt;

&lt;p&gt;When a customer visits a category page, Magento runs roughly this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load the base product collection for the category&lt;/li&gt;
&lt;li&gt;For each filterable attribute, query how many products match each option&lt;/li&gt;
&lt;li&gt;Apply any already-active filters (price, color, size, etc.)&lt;/li&gt;
&lt;li&gt;Render the filter sidebar and product grid&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 2 is where most of the pain lives. With the default MySQL backend, Magento executes a separate query &lt;strong&gt;per filterable attribute&lt;/strong&gt; to count product matches. On a store with 15 filterable attributes and 50,000 products, that's 15+ queries — each potentially scanning millions of EAV rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosing the Bottleneck
&lt;/h2&gt;

&lt;p&gt;First, measure. Add &lt;code&gt;?XDEBUG_SESSION_START=1&lt;/code&gt; or use the built-in Magento profiler to see where time is actually spent.&lt;/p&gt;

&lt;p&gt;For a quick query-level view, enable the query log temporarily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;general_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ON'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;general_log_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/tmp/mysql_general.log'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then load a category page and grep the log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"catalog_product_index_eav&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;catalog_product_index_price"&lt;/span&gt; /tmp/mysql_general.log | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see dozens of queries against &lt;code&gt;catalog_product_index_eav&lt;/code&gt;, that's your confirmation. Disable the log when done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;general_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'OFF'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The EAV Index Tables
&lt;/h3&gt;

&lt;p&gt;Magento uses these index tables for layered navigation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalog_product_index_eav&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text/select attribute options&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalog_product_index_eav_decimal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Decimal attributes (weight, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalog_product_index_price&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Price ranges&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each of these tables has an index on &lt;code&gt;(entity_id, attribute_id, store_id, value)&lt;/code&gt;. If they're fragmented or missing statistics, the MySQL query planner makes bad decisions.&lt;/p&gt;

&lt;p&gt;Check table health:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_index_eav&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_index_eav_decimal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_index_price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;OPTIMIZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_index_eav&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;ANALYZE&lt;/code&gt; weekly via cron on large stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Attribute Configuration Problem
&lt;/h2&gt;

&lt;p&gt;Every attribute marked as "Use in Layered Navigation" costs query time — whether customers actually filter by it or not. This is the single most impactful configuration issue.&lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;Stores → Attributes → Product&lt;/strong&gt; and audit every attribute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use in Layered Navigation&lt;/strong&gt; set to &lt;code&gt;Filterable (with results)&lt;/code&gt; or &lt;code&gt;Filterable (no results)&lt;/code&gt; → runs a count query on every category page load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Used for Sorting in Product Listing&lt;/strong&gt; → adds a sort option but also forces an extra index join&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; If customers don't filter by it, turn it off. Color: yes. Internal SKU suffix: no.&lt;/p&gt;

&lt;p&gt;For attributes that must stay filterable, also check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Attribute source model matters --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;source_model&amp;gt;&lt;/span&gt;Magento\Eav\Model\Entity\Attribute\Source\Table&lt;span class="nt"&gt;&amp;lt;/source_model&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Table-source attributes (custom dropdowns) are much faster than custom source models that call external services on every render.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reindex Strategy
&lt;/h2&gt;

&lt;p&gt;Stale indexes are a hidden killer. Magento 2 has two indexer modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Update on Schedule&lt;/strong&gt; (recommended) — indexes in the background via cron&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update on Save&lt;/strong&gt; — blocks the save operation, dangerous at scale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set all catalog indexes to schedule mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento indexer:set-mode schedule catalog_category_product
bin/magento indexer:set-mode schedule catalog_product_category
bin/magento indexer:set-mode schedule catalog_product_attribute
bin/magento indexer:set-mode schedule catalogrule_product
bin/magento indexer:set-mode schedule catalog_product_price
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check current state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento indexer:status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;catalog_product_attribute&lt;/code&gt; shows &lt;code&gt;invalid&lt;/code&gt;, your layered navigation is doing full-table fallback queries. Reindex it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento indexer:reindex catalog_product_attribute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On large catalogs, run partial reindex during off-peak hours with a dedicated cron job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Elasticsearch / OpenSearch: The Real Fix
&lt;/h2&gt;

&lt;p&gt;If you're still on MySQL-backed layered navigation with more than ~20,000 products, you're fighting the wrong battle. &lt;strong&gt;Elasticsearch or OpenSearch is not optional at that scale.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With ES/OS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All filter counts are computed by the search engine, not MySQL&lt;/li&gt;
&lt;li&gt;Aggregations run in parallel in memory&lt;/li&gt;
&lt;li&gt;Category page load time drops from 3–8s to 200–500ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enable it in &lt;code&gt;app/etc/env.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'system'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'catalog'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'search'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'engine'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'elasticsearch8'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'elasticsearch8_server_hostname'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'elasticsearch8_server_port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'9200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'elasticsearch8_index_prefix'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'magento2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After switching, reindex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento indexer:reindex catalogsearch_fulltext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tuning ES Aggregations
&lt;/h3&gt;

&lt;p&gt;By default, Magento requests all bucket values for each attribute aggregation. On attributes with hundreds of options (e.g., manufacturer), this is expensive. Limit bucket size via the &lt;code&gt;catalog_search&lt;/code&gt; config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;config&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;default&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;catalog&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;search&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;max_suggestions&amp;gt;&lt;/span&gt;5&lt;span class="nt"&gt;&amp;lt;/max_suggestions&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/search&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/catalog&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/default&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For custom attribute sets, consider creating a dedicated Elasticsearch index per store view and using index aliases to swap without downtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching Layered Navigation Results
&lt;/h2&gt;

&lt;p&gt;Even with Elasticsearch, rendering the same filter options for every uncached page request is wasteful. Magento's Full Page Cache handles this for anonymous users, but the cache granularity matters.&lt;/p&gt;

&lt;p&gt;Each unique combination of active filters generates a different cache entry. A category with 10 attributes × 5 options each = 5^10 theoretical cache entries. In practice you'll hit a fraction of those, but cache pollution is real.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vary-By Tuning
&lt;/h3&gt;

&lt;p&gt;Magento uses &lt;code&gt;X-Magento-Vary&lt;/code&gt; as the cache context cookie. Verify it's being set correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://yourstore.com/men/tops.html"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; vary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;Vary: X-Magento-Vary&lt;/code&gt;. If Varnish or nginx is stripping this header, your cache will either over-serve stale pages or never cache properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom Cache Tags for Navigation
&lt;/h3&gt;

&lt;p&gt;For high-traffic stores, consider implementing a custom &lt;code&gt;LayerState&lt;/code&gt; cache around the filter collection loading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getFilters&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'layer_filters_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCurrentCategory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
        &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getActiveFiltersHash&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cached&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$filters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;buildFilters&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filters&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'CATALOG_CATEGORY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'CATALOG_PRODUCT'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="mi"&gt;3600&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$filters&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tag it with &lt;code&gt;CATALOG_PRODUCT&lt;/code&gt; so it invalidates automatically on product changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Price Filter Performance
&lt;/h2&gt;

&lt;p&gt;The price filter is in a class of its own because prices are dynamic — they depend on customer group, catalog rules, special prices, and currency. By default, Magento recalculates price ranges on every request.&lt;/p&gt;

&lt;p&gt;To stabilize this, set price step calculation to manual:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stores → Configuration → Catalog → Layered Navigation → Price Navigation Step Calculation → Manual&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then set a fixed step (e.g., 50). This eliminates the dynamic range query entirely and lets the price filter results be cached normally.&lt;/p&gt;

&lt;p&gt;For stores with customer-group pricing, you'll need to vary the cache by customer group. Add this to your custom module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Plugin on Magento\LayeredNavigation\Block\Navigation&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;afterGetCacheKeyInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Navigation&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customerSession&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCustomerGroupId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Benchmarking Your Changes
&lt;/h2&gt;

&lt;p&gt;Track the impact of each change. A minimal benchmark script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://yourstore.com/men/tops.html"&lt;/span&gt;
&lt;span class="nv"&gt;RUNS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Warming cache..."&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Benchmarking &lt;/span&gt;&lt;span class="nv"&gt;$RUNS&lt;/span&gt;&lt;span class="s2"&gt; runs..."&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 &lt;span class="nv"&gt;$RUNS&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;time &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Cache-Control: no-cache"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better: use &lt;code&gt;k6&lt;/code&gt; or &lt;code&gt;wrk&lt;/code&gt; to simulate concurrent users and see how the database holds up under load. The real problem often only shows up with 10+ concurrent category page requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Wins Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Remove unused filterable attributes&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run &lt;code&gt;ANALYZE TABLE&lt;/code&gt; on index tables&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Switch to Update on Schedule indexing&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable Elasticsearch/OpenSearch&lt;/td&gt;
&lt;td&gt;Very High&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set manual price navigation step&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add customer-group cache variation&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom filter result caching&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Slow category pages are almost always a layered navigation problem. Start with the easy wins: audit your filterable attributes, fix your indexer mode, and run ANALYZE on your index tables. If you're beyond 20k products, move to Elasticsearch — it's the single biggest lever available.&lt;/p&gt;

&lt;p&gt;The goal is consistent sub-500ms category page loads regardless of filter combinations. With the right configuration and a proper search backend, that's absolutely achievable.&lt;/p&gt;

&lt;p&gt;Next up: we'll look at how Magento's price indexing interacts with catalog rules and why it can cause full reindexes to run during peak hours — and what to do about it.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>performance</category>
      <category>php</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Store Emulation: The Hidden Performance Killer in Your Codebase</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Fri, 22 May 2026 09:02:34 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-store-emulation-the-hidden-performance-killer-in-your-codebase-21ob</link>
      <guid>https://dev.to/magevanta/magento-2-store-emulation-the-hidden-performance-killer-in-your-codebase-21ob</guid>
      <description>&lt;p&gt;Every Magento 2 developer has used it at some point: &lt;code&gt;Magento\Store\Model\App\Emulation&lt;/code&gt;. It looks innocent, it solves a real problem, and it ships with Magento itself. But in production, store emulation is one of the most overlooked causes of slow checkout, sluggish transactional emails, and mysterious cart rule performance issues.&lt;/p&gt;

&lt;p&gt;In this post we'll tear apart how store emulation works under the hood, measure the real cost, show you how to detect it in your codebase, and give you battle-tested patterns to avoid it where possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Store Emulation?
&lt;/h2&gt;

&lt;p&gt;Magento's multi-store architecture means every object — products, prices, URLs, translations — can differ per store view. The problem arises when you need to render something &lt;em&gt;in the context of a specific store&lt;/em&gt; while PHP is executing in a different one.&lt;/p&gt;

&lt;p&gt;The classic case: a cron job running under the admin scope needs to send an order confirmation email in the customer's original store language, with that store's logo, sender address, and translated template.&lt;/p&gt;

&lt;p&gt;Enter &lt;code&gt;Magento\Store\Model\App\Emulation&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;appEmulation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;startEnvironmentEmulation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$storeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;\Magento\Framework\App\Area&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AREA_FRONTEND&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Render email, generate URLs, translate strings...&lt;/span&gt;

&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;appEmulation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;stopEnvironmentEmulation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API is simple. The cost is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Store Emulation Is Expensive
&lt;/h2&gt;

&lt;p&gt;When you call &lt;code&gt;startEnvironmentEmulation()&lt;/code&gt;, Magento performs a cascade of state changes:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Design Theme Switch
&lt;/h3&gt;

&lt;p&gt;Magento resolves and loads the theme for the target store, including all registered design changes, theme inheritance chains, and XML merge results. If you emulate 5 different stores in a loop, this happens 5 times — with full config reads from the database each time.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Translation Model Reset
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Magento\Framework\Translate&lt;/code&gt; model is reloaded for the target locale. This means reading translation CSV files from disk, parsing them into memory, and rebuilding the in-memory phrase dictionary. For a store with 50,000 translation entries, this is not free.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Config Scope Change
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;ScopeConfigInterface&lt;/code&gt; switches to the target store's scope. Any code that reads config during the emulation will retrieve values specific to that store, which can bypass or invalidate in-memory cached config values.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Event Area Change
&lt;/h3&gt;

&lt;p&gt;The current event area is changed to &lt;code&gt;frontend&lt;/code&gt;, which affects which observers fire during execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Cleanup on Stop
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;stopEnvironmentEmulation()&lt;/code&gt; reverses all of the above — re-loading the original theme, locale, and config scope.&lt;/p&gt;

&lt;p&gt;The total cost per emulation cycle is typically &lt;strong&gt;50–200ms&lt;/strong&gt; depending on theme complexity, locale size, and cache warmth. In a loop over 1,000 orders, that's up to &lt;strong&gt;3 minutes&lt;/strong&gt; of pure overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting Store Emulation in Your Codebase
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Search for Direct Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"startEnvironmentEmulation"&lt;/span&gt; app/code/ vendor/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pay close attention to any results inside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Cron/&lt;/code&gt; directories&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Observer/&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Plugin/&lt;/code&gt; files wrapping email or PDF generators&lt;/li&gt;
&lt;li&gt;Any code that runs in a loop&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Profile with Blackfire or New Relic
&lt;/h3&gt;

&lt;p&gt;Look for repeated calls to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Magento\Framework\Translate::loadData&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Magento\Theme\Model\Design::getConfigurationDesignTheme&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Magento\Store\Model\App\Emulation::startEnvironmentEmulation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Blackfire trace on a bulk email send job will often show emulation overhead as the #1 wall time consumer, dwarfing database queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enable Query Logging
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In a development environment&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Emulation start'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'store_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$storeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'memory'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;memory_get_usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log before and after emulation blocks. If you see &lt;code&gt;memory_get_usage&lt;/code&gt; jumping by 10–20MB per cycle, translation loading is the culprit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Anti-Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Anti-Pattern 1: Emulation Inside a Loop
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Never do this&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$orders&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;appEmulation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;startEnvironmentEmulation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStoreId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;emailSender&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;appEmulation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;stopEnvironmentEmulation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each iteration reloads translations and theme config. For 500 orders, you're doing 500 full environment resets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Group orders by store ID and emulate once per store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Group by store, emulate once&lt;/span&gt;
&lt;span class="nv"&gt;$ordersByStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$orders&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$ordersByStore&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStoreId&lt;/span&gt;&lt;span class="p"&gt;()][]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ordersByStore&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$storeId&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$storeOrders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;appEmulation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;startEnvironmentEmulation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$storeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$storeOrders&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;emailSender&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;appEmulation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;stopEnvironmentEmulation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Anti-Pattern 2: Emulating for Config Reads Only
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Overkill — full emulation just to read a store config value&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;appEmulation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;startEnvironmentEmulation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$storeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;scopeConfig&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my/config/path'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;appEmulation&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;stopEnvironmentEmulation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Pass the store scope directly to &lt;code&gt;ScopeConfigInterface&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ No emulation needed&lt;/span&gt;
&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;scopeConfig&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'my/config/path'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;\Magento\Store\Model\ScopeInterface&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SCOPE_STORE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$storeId&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reads the store-scoped config directly without any environment switching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anti-Pattern 3: Emulating in Observers
&lt;/h3&gt;

&lt;p&gt;If you have an observer on &lt;code&gt;sales_order_place_after&lt;/code&gt; and it triggers emulation to generate a custom PDF or send a custom notification, you're adding emulation overhead to the checkout critical path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Move the work to an async queue consumer using &lt;code&gt;Magento\Framework\MessageQueue&lt;/code&gt;. Emit an event, enqueue the job, handle it in a consumer where emulation cost does not block the HTTP request.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Emulation Is Actually Necessary
&lt;/h2&gt;

&lt;p&gt;To be fair, there are legitimate cases where emulation is unavoidable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rendering email templates with block HTML&lt;/strong&gt; — &lt;code&gt;\Magento\Email\Model\Template::getProcessedTemplate()&lt;/code&gt; requires the correct store context to resolve template variables, store URLs, and media base URLs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generating PDFs with store-specific fonts/logos&lt;/strong&gt; — the PDF renderer reads theme paths&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translating strings with &lt;code&gt;__()&lt;/code&gt; helper&lt;/strong&gt; — the translation function uses the globally loaded locale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these cases, use emulation — but apply the grouping pattern above and keep the emulated block as narrow as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Emulation-Free Email Pattern
&lt;/h2&gt;

&lt;p&gt;One powerful alternative for email rendering is to pass store context explicitly rather than relying on global state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Instead of emulating, pass store ID directly to the transport builder&lt;/span&gt;
&lt;span class="nv"&gt;$transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;transportBuilder&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setTemplateIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$templateId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setTemplateOptions&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'area'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\Magento\Framework\App\Area&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AREA_FRONTEND&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'store'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$storeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// ← Context passed here, not via emulation&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setTemplateVars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$vars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$recipientEmail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTransport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$transport&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TransportBuilder::setTemplateOptions()&lt;/code&gt; accepts a store ID and loads the template in that store's context without triggering a full environment emulation. This is the recommended approach for programmatic email sending in Magento 2.4+.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the Improvement
&lt;/h2&gt;

&lt;p&gt;Here's a real-world benchmark from a Magento 2.4.6 store with 12 store views, running a bulk reorder notification cron:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;1,000 orders (12 stores)&lt;/th&gt;
&lt;th&gt;Memory peak&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Emulation per order&lt;/td&gt;
&lt;td&gt;4 min 12 sec&lt;/td&gt;
&lt;td&gt;512 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emulation per store (grouped)&lt;/td&gt;
&lt;td&gt;38 sec&lt;/td&gt;
&lt;td&gt;320 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TransportBuilder with store options&lt;/td&gt;
&lt;td&gt;22 sec&lt;/td&gt;
&lt;td&gt;290 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Grouping alone delivered an &lt;strong&gt;86% reduction in execution time&lt;/strong&gt;. The emulation-free approach pushed it further.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auditing Third-Party Modules
&lt;/h2&gt;

&lt;p&gt;This is where things get painful. Many third-party modules use store emulation carelessly. Run the grep command above against your &lt;code&gt;vendor/&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"startEnvironmentEmulation"&lt;/span&gt; vendor/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"Test"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each result, open the file and check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is it called inside a loop?&lt;/li&gt;
&lt;li&gt;Is it called during a synchronous HTTP request?&lt;/li&gt;
&lt;li&gt;Could a queued approach work instead?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you find a third-party module doing emulation per-order in a loop, open an issue or patch it yourself using a plugin. Wrapping the method to add grouping logic is usually straightforward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Wins Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never emulate in a loop per-object&lt;/strong&gt; — always group by store ID first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;scopeConfig-&amp;gt;getValue()&lt;/code&gt; with explicit store scope&lt;/strong&gt; instead of emulating for config reads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;TransportBuilder::setTemplateOptions(['store' =&amp;gt; $storeId])&lt;/code&gt;&lt;/strong&gt; for emails where possible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move emulation-heavy work to async queue consumers&lt;/strong&gt; to keep checkout fast&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit your vendor modules&lt;/strong&gt; — third-party code is often the worst offender&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile with Blackfire&lt;/strong&gt; — emulation overhead is invisible without it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Store emulation is a tool, not a crutch. Used carefully and sparingly, it solves real multi-store rendering challenges. Used naively — especially in loops — it silently kills performance at exactly the worst moments: high-volume cron jobs, batch processing, and post-checkout flows.&lt;/p&gt;

&lt;p&gt;Audit your codebase. Find the loops. Group the emulation. Your servers will thank you.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 REST API Performance: Bulk Endpoints, Async Operations &amp; Optimization</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Thu, 21 May 2026 09:02:34 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-rest-api-performance-bulk-endpoints-async-operations-optimization-2ek0</link>
      <guid>https://dev.to/magevanta/magento-2-rest-api-performance-bulk-endpoints-async-operations-optimization-2ek0</guid>
      <description>&lt;p&gt;If you're integrating an ERP, a PIM, or a third-party service with your Magento 2 store, the REST API is probably your first instinct. It's convenient, well-documented, and powerful. It's also one of the easiest ways to accidentally bring your store to its knees.&lt;/p&gt;

&lt;p&gt;Slow API calls, thousands of individual product updates per hour, token generation overhead on every single request — these are patterns we see constantly in real-world Magento deployments. This guide will show you how to avoid the most common pitfalls and get serious performance out of the Magento 2 REST API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Magento 2 API Performance Matters
&lt;/h2&gt;

&lt;p&gt;Unlike a frontend page load, API performance issues are invisible to end users — right up until they aren't. A sync process that creates 500 products one-by-one can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Max out PHP-FPM workers, starving frontend visitors of threads&lt;/li&gt;
&lt;li&gt;Trigger thousands of reindex events in rapid succession&lt;/li&gt;
&lt;li&gt;Lock database tables, causing checkout timeouts&lt;/li&gt;
&lt;li&gt;Spike CPU usage and exhaust connections in MySQL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The REST API doesn't have a separate resource pool by default. Every &lt;code&gt;/rest/V1/products&lt;/code&gt; call is a full Magento bootstrap hitting the same stack as your storefront.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Use Bulk Endpoints Instead of Looping Single Calls
&lt;/h2&gt;

&lt;p&gt;The biggest mistake in Magento API integrations is making individual calls in a loop. Most developers do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terrible: 500 separate HTTP requests&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;product &lt;span class="k"&gt;in &lt;/span&gt;products:
    POST /rest/V1/products &lt;span class="o"&gt;{&lt;/span&gt; product data &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Magento 2.3+ ships with &lt;strong&gt;Async Bulk REST&lt;/strong&gt; endpoints that batch operations into a single request and process them via message queues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Good: One HTTP request, processed async&lt;/span&gt;
POST /rest/async/bulk/V1/products
Content-Type: application/json

&lt;span class="o"&gt;[&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"product"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"sku"&lt;/span&gt;: &lt;span class="s2"&gt;"PROD-001"&lt;/span&gt;, &lt;span class="s2"&gt;"price"&lt;/span&gt;: 29.99 &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"product"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"sku"&lt;/span&gt;: &lt;span class="s2"&gt;"PROD-002"&lt;/span&gt;, &lt;span class="s2"&gt;"price"&lt;/span&gt;: 49.99 &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"product"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"sku"&lt;/span&gt;: &lt;span class="s2"&gt;"PROD-003"&lt;/span&gt;, &lt;span class="s2"&gt;"price"&lt;/span&gt;: 19.99 &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint returns immediately with a &lt;code&gt;bulk_uuid&lt;/code&gt; and queues the operations for processing by Magento's message queue consumers. Your integration gets a 200 response in milliseconds instead of waiting for 500 sequential database writes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bulk Endpoint Format
&lt;/h3&gt;

&lt;p&gt;The pattern for bulk async endpoints is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /rest/async/bulk/V1/{resource}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for most standard endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/rest/async/bulk/V1/products&lt;/code&gt; — create/update products&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/rest/async/bulk/V1/inventory/source-items&lt;/code&gt; — update stock&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/rest/async/bulk/V1/orders/{id}/ship&lt;/code&gt; — create shipments&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/rest/async/bulk/V1/customers&lt;/code&gt; — create/update customers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Checking Bulk Operation Status
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /rest/V1/bulk/&lt;span class="o"&gt;{&lt;/span&gt;bulkUuid&lt;span class="o"&gt;}&lt;/span&gt;/status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns per-operation status so you can retry any failed items without reprocessing the whole batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Stop Generating New Tokens on Every Request
&lt;/h2&gt;

&lt;p&gt;Integration tokens are expensive — Magento goes through a full authentication flow including database lookups and password hashing. We've seen integrations that generate a new token before every single API call. At 1000 calls per hour that's 1000 unnecessary auth cycles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache your tokens.&lt;/strong&gt; Integration tokens are valid for 1 hour by default (configurable). Store the token and reuse it until it expires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApiClient&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$cachedToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$tokenExpiry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cachedToken&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tokenExpiry&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cachedToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/rest/V1/integration/admin/token'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'username'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cachedToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tokenExpiry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1 hour&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cachedToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production integrations, store the token in Redis with a TTL of 55 minutes (5-minute safety margin). Multiple workers will then share the same token instead of each generating their own.&lt;/p&gt;

&lt;p&gt;You can also adjust the token lifetime in &lt;strong&gt;Stores → Configuration → Services → OAuth → Access Token Expiration&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Increase Token Lifetime for Long-Running Processes
&lt;/h2&gt;

&lt;p&gt;The default 1-hour token lifetime is often too short for batch jobs. You can increase it under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stores → Configuration → Services → OAuth → Access Token Expiration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For dedicated integration users, setting this to 24 hours or even longer (set to 0 for no expiration) eliminates token refresh overhead entirely. Just be sure to rotate credentials periodically and revoke tokens for retired integrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Enable Response Compression
&lt;/h2&gt;

&lt;p&gt;Most Magento 2 API responses are uncompressed by default. A product listing response with 100 products can easily be 200–400KB of JSON. Enable gzip compression in Nginx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /etc/nginx/conf.d/magento.conf&lt;/span&gt;
&lt;span class="k"&gt;gzip&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;gzip_types&lt;/span&gt; &lt;span class="nc"&gt;application/json&lt;/span&gt; &lt;span class="nc"&gt;text/plain&lt;/span&gt; &lt;span class="nc"&gt;application/xml&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;gzip_min_length&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;gzip_comp_level&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure your integration client sends &lt;code&gt;Accept-Encoding: gzip&lt;/code&gt; — most HTTP libraries do this automatically. For large product catalogs, this alone can reduce API transfer time by 70–80%.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Use &lt;code&gt;fields&lt;/code&gt; to Limit Response Payload
&lt;/h2&gt;

&lt;p&gt;Magento supports field filtering on GET requests via the &lt;code&gt;fields&lt;/code&gt; parameter. Instead of getting 80+ fields back for every product, ask only for what you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Returns full product objects (heavy)&lt;/span&gt;
GET /rest/V1/products?searchCriteria[pageSize]&lt;span class="o"&gt;=&lt;/span&gt;100

&lt;span class="c"&gt;# Returns only sku, price, and qty (fast)&lt;/span&gt;
GET /rest/V1/products?searchCriteria[pageSize]&lt;span class="o"&gt;=&lt;/span&gt;100&amp;amp;fields&lt;span class="o"&gt;=&lt;/span&gt;items[sku,price,extension_attributes[stock_item[qty]]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduces both the SQL query complexity (fewer EAV attribute joins) and the response payload size. For inventory-only syncs, this can be a 10× improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Tune searchCriteria for Efficient Queries
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;GET /rest/V1/products&lt;/code&gt; call with &lt;code&gt;searchCriteria&lt;/code&gt; translates to a MySQL query. Poorly constructed criteria can result in full table scans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid unindexed fields in filters:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Slow — no index on created_at for most product queries&lt;/span&gt;
GET /rest/V1/products?searchCriteria[filterGroups][0][filters][0][field]&lt;span class="o"&gt;=&lt;/span&gt;created_at&amp;amp;...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Better — filter on indexed fields:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Fast — sku and entity_id are indexed&lt;/span&gt;
GET /rest/V1/products?searchCriteria[filterGroups][0][filters][0][field]&lt;span class="o"&gt;=&lt;/span&gt;sku&amp;amp;searchCriteria[filterGroups][0][filters][0][value]&lt;span class="o"&gt;=&lt;/span&gt;PROD-%&amp;amp;searchCriteria[filterGroups][0][filters][0][condition_type]&lt;span class="o"&gt;=&lt;/span&gt;like
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For large catalogs, always:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filter by indexed fields where possible (&lt;code&gt;sku&lt;/code&gt;, &lt;code&gt;entity_id&lt;/code&gt;, &lt;code&gt;type_id&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;pageSize&lt;/code&gt; + &lt;code&gt;currentPage&lt;/code&gt; for pagination instead of loading everything at once&lt;/li&gt;
&lt;li&gt;Implement cursor-based pagination using &lt;code&gt;entity_id&lt;/code&gt; greater-than filters for more predictable performance&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Run Async Consumers on Dedicated Workers
&lt;/h2&gt;

&lt;p&gt;If you're using the bulk async API, make sure your message queue consumers are actually running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check running consumers&lt;/span&gt;
ps aux | &lt;span class="nb"&gt;grep &lt;/span&gt;queue:consumers:start

&lt;span class="c"&gt;# Start a consumer for async operations&lt;/span&gt;
bin/magento queue:consumers:start async.operations.all &lt;span class="nt"&gt;--max-messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10000 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, manage consumers with &lt;strong&gt;Supervisor&lt;/strong&gt; to ensure they restart on failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/supervisor/conf.d/magento-async.conf
&lt;/span&gt;&lt;span class="nn"&gt;[program:magento-async]&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;php /var/www/html/bin/magento queue:consumers:start async.operations.all --max-messages=10000&lt;/span&gt;
&lt;span class="py"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/www/html&lt;/span&gt;
&lt;span class="py"&gt;autostart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;autorestart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;stderr_logfile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/log/magento-async.err.log&lt;/span&gt;
&lt;span class="py"&gt;stdout_logfile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/log/magento-async.out.log&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without running consumers, bulk API calls queue up indefinitely and nothing gets processed.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Disable Synchronous Reindexing During Bulk Operations
&lt;/h2&gt;

&lt;p&gt;By default, Magento may trigger index updates after each API write. For bulk imports, this is catastrophic. Switch your affected indexers to &lt;strong&gt;scheduled&lt;/strong&gt; mode before running large syncs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Switch to scheduled (async) reindexing&lt;/span&gt;
bin/magento indexer:set-mode schedule cataloginventory_stock catalog_product_price

&lt;span class="c"&gt;# Verify&lt;/span&gt;
bin/magento indexer:show-mode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run a full reindex after your bulk operation completes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento indexer:reindex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This alone can cut bulk import time by 40–60% for catalog operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Rate Limit Inbound API Traffic
&lt;/h2&gt;

&lt;p&gt;If you can't control what third parties send, protect your stack with Nginx rate limiting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=api:10m&lt;/span&gt; &lt;span class="s"&gt;rate=30r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/rest/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=api&lt;/span&gt; &lt;span class="s"&gt;burst=50&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;limit_req_status&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# ... rest of config&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents a rogue integration from flooding your API and starving frontend visitors of PHP workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Reference: API Performance Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Looping single creates/updates&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;/rest/async/bulk/V1/{resource}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token generated on every call&lt;/td&gt;
&lt;td&gt;Cache token in Redis with 55min TTL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large response payloads&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;&amp;amp;fields=items[sku,price]&lt;/code&gt; to requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full reindex on every write&lt;/td&gt;
&lt;td&gt;Switch indexers to &lt;code&gt;schedule&lt;/code&gt; mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumers not running&lt;/td&gt;
&lt;td&gt;Set up Supervisor for &lt;code&gt;async.operations.all&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No compression on responses&lt;/td&gt;
&lt;td&gt;Enable gzip in Nginx for &lt;code&gt;application/json&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unprotected API endpoint&lt;/td&gt;
&lt;td&gt;Add Nginx &lt;code&gt;limit_req&lt;/code&gt; rate limiting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;For a typical ERP integration syncing 5000 products per hour, the difference between naïve and optimized approaches is staggering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Naïve approach&lt;/strong&gt;: 5000 HTTP requests × ~200ms each = ~17 minutes, 5000 reindex triggers, PHP workers saturated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimized approach&lt;/strong&gt;: 50 bulk requests × ~50ms each = ~3 seconds, 1 reindex run after sync, zero frontend impact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The REST API is powerful when used correctly. Batch aggressively, cache tokens, compress responses, and keep your index consumers running — your integration performance will follow.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>api</category>
    </item>
    <item>
      <title>Magento 2 URL Rewrite Performance: Diagnose and Fix Slow URL Routing</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Wed, 20 May 2026 09:02:33 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-url-rewrite-performance-diagnose-and-fix-slow-url-routing-7a</link>
      <guid>https://dev.to/magevanta/magento-2-url-rewrite-performance-diagnose-and-fix-slow-url-routing-7a</guid>
      <description>&lt;p&gt;Every Magento 2 store relies on URL rewrites to map human-readable URLs like &lt;code&gt;/red-running-shoes.html&lt;/code&gt; to internal product routes. On a fresh install with a small catalog, this works seamlessly. But on stores with thousands of SKUs, multiple store views, and years of category restructuring behind them, the &lt;code&gt;url_rewrite&lt;/code&gt; table becomes a graveyard of obsolete entries — and it quietly drags down every page request.&lt;/p&gt;

&lt;p&gt;This guide covers how URL rewrites work under the hood, why they degrade over time, and the concrete steps to reclaim that lost performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Magento 2 URL Routing Actually Works
&lt;/h2&gt;

&lt;p&gt;When a request hits Magento, the router pipeline runs through several router classes in sequence. For most frontend URLs, the &lt;strong&gt;&lt;code&gt;Magento\UrlRewrite\Controller\Router&lt;/code&gt;&lt;/strong&gt; is queried early. It performs a &lt;code&gt;SELECT&lt;/code&gt; against the &lt;code&gt;url_rewrite&lt;/code&gt; table to match the incoming request path to a target path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;url_rewrite&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;request_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-category/your-product.html'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a well-maintained store, this query is fast. On a bloated table with 500,000+ rows and missing indexes, it becomes a measurable bottleneck hit on every uncached page load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Table Grows Out of Control
&lt;/h2&gt;

&lt;p&gt;Magento generates URL rewrites automatically for products, categories, and CMS pages. It also preserves old URLs as redirects when you rename or move things. This is good for SEO — but it accumulates fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product URL key changes&lt;/strong&gt;: Every time a product URL key changes, Magento creates a 301 redirect entry for the old path and a new canonical entry. Change 1,000 products twice and you have 3,000 rows minimum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Category tree restructuring&lt;/strong&gt;: Moving a category creates cascading rewrites for all child categories and their associated products across every affected nesting level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-store setups&lt;/strong&gt;: Each store view gets its own rewrite entries. A 50,000-product catalog with 3 store views means 150,000+ product rows alone — before any historic redirects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import/export side effects&lt;/strong&gt;: Bulk product imports using tools that don't respect existing URL keys will regenerate rewrites wholesale, sometimes leaving orphaned duplicates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party modules&lt;/strong&gt;: Some catalog management or bulk-edit extensions trigger rewrite regeneration on every save, multiplying the bloat.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A store that has been running for 2–3 years can easily have 1–2 million rows in &lt;code&gt;url_rewrite&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosing the Problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Check the Table Size
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;table_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;data_length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;index_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;size_mb&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'url_rewrite'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything above 500MB or 500,000 rows warrants investigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Count by Entity Type and Store
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;entity_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;redirect_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;url_rewrite&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;entity_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redirect_type&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This quickly reveals where the bloat lives. A &lt;code&gt;redirect_type&lt;/code&gt; of &lt;code&gt;301&lt;/code&gt; means it's a historic redirect. High counts here are usually safe to prune after verifying.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Find Orphaned Product Rewrites
&lt;/h3&gt;

&lt;p&gt;Products that have been deleted leave behind their rewrite entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_rewrite_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_path&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;url_rewrite&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity&lt;/span&gt; &lt;span class="n"&gt;cpe&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cpe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'product'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;cpe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Identify the Slow Query in New Relic or MySQL Slow Log
&lt;/h3&gt;

&lt;p&gt;Enable the MySQL slow query log if you haven't already:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;slow_query_log&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;slow_query_log_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/mysql/slow.log&lt;/span&gt;
&lt;span class="py"&gt;long_query_time&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0.5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search for queries on &lt;code&gt;url_rewrite&lt;/code&gt; running above 50ms consistently. These are your signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixes: From Quick Wins to Full Overhauls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fix 1: Disable Automatic 301 Preservation (High Impact, Low Risk)
&lt;/h3&gt;

&lt;p&gt;By default, Magento saves old URLs as 301 redirects when you change a URL key. For most stores, this accumulates thousands of unnecessary redirects. Unless you have a real SEO need for them, disable it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin → Stores → Configuration → Catalog → Search Engine Optimization → Create Permanent Redirect for URLs if URL Key Changed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set this to &lt;strong&gt;No&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This stops new bloat from accumulating immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 2: Clean Up Orphaned and Duplicate Rewrites
&lt;/h3&gt;

&lt;p&gt;Use a maintenance script during off-peak hours. Always take a backup first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remove orphaned product rewrites:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;url_rewrite&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity&lt;/span&gt; &lt;span class="n"&gt;cpe&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cpe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'product'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;cpe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Remove orphaned category rewrites:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;url_rewrite&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;catalog_category_entity&lt;/span&gt; &lt;span class="n"&gt;cce&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;cce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Remove duplicate request paths (keep the newest):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="n"&gt;ur1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;url_rewrite&lt;/span&gt; &lt;span class="n"&gt;ur1&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;url_rewrite&lt;/span&gt; &lt;span class="n"&gt;ur2&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ur1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ur2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_path&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;ur1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ur2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store_id&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;ur1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_rewrite_id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;ur2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_rewrite_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fix 3: Verify and Optimize Table Indexes
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;url_rewrite&lt;/code&gt; table ships with indexes, but they can degrade or be accidentally dropped by module installers. Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;url_rewrite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need at minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A unique key on &lt;code&gt;(request_path, store_id)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;An index on &lt;code&gt;(target_path)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;An index on &lt;code&gt;(entity_type, entity_id, store_id)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If missing, add them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;url_rewrite&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;UNQ_REQUEST_PATH_STORE_ID&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IDX_TARGET_PATH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IDX_ENTITY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After cleaning the table, run &lt;code&gt;OPTIMIZE TABLE url_rewrite;&lt;/code&gt; to reclaim space and rebuild the index fragmentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 4: Regenerate Rewrites Selectively
&lt;/h3&gt;

&lt;p&gt;After a cleanup, you may want to regenerate rewrites for a specific store view or category tree rather than the whole catalog. Use the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Full regeneration (slow on large catalogs — run during maintenance window)&lt;/span&gt;
bin/magento indexer:reindex url_rewrite

&lt;span class="c"&gt;# Or reindex all indexers&lt;/span&gt;
bin/magento indexer:reindex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For large catalogs, consider regenerating by store view using a custom script or a module like &lt;strong&gt;MageWorx URL Rewrites Manager&lt;/strong&gt; which supports selective regeneration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 5: Limit Category-Product URL Rewrites
&lt;/h3&gt;

&lt;p&gt;One of the biggest contributors to table bloat is category-path rewrites for products. Magento by default generates a rewrite for every category a product belongs to, in every store view.&lt;/p&gt;

&lt;p&gt;Disable this if you don't need category paths in product URLs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin → Stores → Configuration → Catalog → Search Engine Optimization → Use Categories Path for Product URLs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set to &lt;strong&gt;No&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This alone can reduce your rewrite table by 30–60% on stores with complex category structures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing Future Bloat
&lt;/h2&gt;

&lt;p&gt;Once you've cleaned up, keep it clean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automate orphan cleanup&lt;/strong&gt;: Schedule a weekly cron job that runs the orphan-deletion queries on staging, validates the output, then runs on production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor table size&lt;/strong&gt;: Add a Grafana/New Relic alert when &lt;code&gt;url_rewrite&lt;/code&gt; exceeds a row count threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review before bulk imports&lt;/strong&gt;: Ensure your import scripts set consistent URL keys and don't trigger mass rewrite regeneration unnecessarily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a staging environment for category restructuring&lt;/strong&gt;: Preview rewrite generation before applying to production.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Benchmarking the Results
&lt;/h2&gt;

&lt;p&gt;After a cleanup on a real store (150,000 products, 3 store views, ~2.1M rows), here are representative results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;url_rewrite&lt;/code&gt; row count&lt;/td&gt;
&lt;td&gt;2,100,000&lt;/td&gt;
&lt;td&gt;390,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table size&lt;/td&gt;
&lt;td&gt;1.8 GB&lt;/td&gt;
&lt;td&gt;340 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg. router query time&lt;/td&gt;
&lt;td&gt;180ms&lt;/td&gt;
&lt;td&gt;12ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFB (uncached PDP)&lt;/td&gt;
&lt;td&gt;820ms&lt;/td&gt;
&lt;td&gt;590ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFB (uncached PLP)&lt;/td&gt;
&lt;td&gt;1.1s&lt;/td&gt;
&lt;td&gt;760ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The TTFB improvement compounds with other optimizations — a faster router query means Magento's dispatch cycle finishes sooner, benefiting every subsequent operation in the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;URL rewrites are essential for SEO-friendly Magento stores, but they're also one of the most commonly neglected performance vectors. The fix isn't complicated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stop creating unnecessary 301s on URL key changes&lt;/li&gt;
&lt;li&gt;Remove orphaned and duplicate entries&lt;/li&gt;
&lt;li&gt;Ensure indexes are intact and optimized&lt;/li&gt;
&lt;li&gt;Disable category-path product rewrites if not needed&lt;/li&gt;
&lt;li&gt;Monitor and automate cleanup going forward&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A clean &lt;code&gt;url_rewrite&lt;/code&gt; table means faster routing on every single uncached request — and that's a win that scales with your traffic.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>performance</category>
      <category>mysql</category>
      <category>php</category>
    </item>
    <item>
      <title>Magento 2 Plugins &amp; Observers: Avoiding Hidden Performance Bottlenecks</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Tue, 19 May 2026 09:02:11 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-plugins-observers-avoiding-hidden-performance-bottlenecks-4d71</link>
      <guid>https://dev.to/magevanta/magento-2-plugins-observers-avoiding-hidden-performance-bottlenecks-4d71</guid>
      <description>&lt;p&gt;Magento 2's plugin and observer systems are among its most powerful extension mechanisms. They let you hook into virtually any public method or event without modifying core code. But with great power comes great responsibility — and a surprising number of production sites suffer from slow page loads, high TTFB, and sluggish admin panels because of poorly written plugins and observers scattered across installed modules.&lt;/p&gt;

&lt;p&gt;This post digs into how these extension points work under the hood, how to measure their impact, and what you can do to keep your stack lean and fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Plugins Work Internally
&lt;/h2&gt;

&lt;p&gt;Every time you define a &lt;code&gt;&amp;lt;plugin&amp;gt;&lt;/code&gt; in &lt;code&gt;di.xml&lt;/code&gt;, Magento generates an &lt;strong&gt;interceptor class&lt;/strong&gt; that wraps the original class. On the first request (or after &lt;code&gt;setup:di:compile&lt;/code&gt;), the framework builds these proxy classes in &lt;code&gt;generated/code/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An interceptor works roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generated interceptor pseudocode&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;someMethod&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// call all "before" plugins&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pluginList&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'before'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$plugin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$plugin&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;beforeSomeMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// call "around" plugins (recursive chain)&lt;/span&gt;
    &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$aroundPlugin&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;aroundSomeMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// call all "after" plugins&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pluginList&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'after'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$plugin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$plugin&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;afterSomeMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cost here is &lt;strong&gt;not negligible&lt;/strong&gt;. Every plugin adds method calls, closure creation, and argument passing. When a method like &lt;code&gt;\Magento\Catalog\Model\Product::getData()&lt;/code&gt; — called hundreds of times per page — has five interceptors stacked on it, the overhead compounds quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Around Plugins: The Most Dangerous Type
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;around&lt;/code&gt; plugins are the heaviest interceptor type. They wrap the original method in a closure (&lt;code&gt;$proceed&lt;/code&gt;), meaning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The full call stack depth increases&lt;/li&gt;
&lt;li&gt;PHP must maintain additional stack frames&lt;/li&gt;
&lt;li&gt;Closures are slower than direct method calls&lt;/li&gt;
&lt;li&gt;Other plugins in the chain still run through the same closure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The rule of thumb:&lt;/strong&gt; Only use &lt;code&gt;around&lt;/code&gt; when you need to conditionally skip the original method entirely. For everything else, prefer &lt;code&gt;before&lt;/code&gt; or &lt;code&gt;after&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Bad pattern — using &lt;code&gt;around&lt;/code&gt; just to modify the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't do this&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;aroundGetName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Product&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;callable&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;strtoupper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better — use &lt;code&gt;after&lt;/code&gt; instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;afterGetName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Product&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;strtoupper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is small per call, but if this runs on a category page listing 48 products, you've already added 48 unnecessary closure invocations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profiling Plugin Overhead
&lt;/h2&gt;

&lt;p&gt;The fastest way to spot plugin bloat is to use the built-in Magento profiler or Blackfire/Xdebug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable the built-in profiler:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In pub/index.php, add at the top:&lt;/span&gt;
&lt;span class="se"&gt;\M&lt;/span&gt;agento&lt;span class="se"&gt;\F&lt;/span&gt;ramework&lt;span class="se"&gt;\P&lt;/span&gt;rofiler::start&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'root'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;# Or via env variable:&lt;/span&gt;
&lt;span class="nv"&gt;MAGE_PROFILER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;html php bin/magento ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for methods that appear hundreds of times in the call graph. Then cross-reference with &lt;code&gt;generated/code/&lt;/code&gt; to see how many interceptor layers they have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using the CLI to list active plugins:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find all plugins registered for a class&lt;/span&gt;
bin/magento dev:di:info &lt;span class="s2"&gt;"Magento&lt;/span&gt;&lt;span class="se"&gt;\C&lt;/span&gt;&lt;span class="s2"&gt;atalog&lt;/span&gt;&lt;span class="se"&gt;\M&lt;/span&gt;&lt;span class="s2"&gt;odel&lt;/span&gt;&lt;span class="se"&gt;\P&lt;/span&gt;&lt;span class="s2"&gt;roduct"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This outputs a table of all plugins, their types (before/around/after), and sort order. If you see 10+ plugins on a hot method, that's your culprit.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Observers Add Up
&lt;/h2&gt;

&lt;p&gt;Observers work differently — they're event-based and asynchronous in feel, but they run &lt;strong&gt;synchronously&lt;/strong&gt; in the same PHP process. Every &lt;code&gt;$eventManager-&amp;gt;dispatch('catalog_product_load_after', ...)&lt;/code&gt; iterates all registered observers for that event.&lt;/p&gt;

&lt;p&gt;The problem compounds when:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Third-party modules register observers on high-frequency events&lt;/strong&gt; like &lt;code&gt;catalog_product_load_after&lt;/code&gt;, &lt;code&gt;layout_generate_blocks_after&lt;/code&gt;, or &lt;code&gt;controller_action_predispatch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observers do heavy work&lt;/strong&gt;: database queries, external API calls, file I/O&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observers aren't guarded&lt;/strong&gt;: they run on every store, every customer group, every page load&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check your &lt;code&gt;events.xml&lt;/code&gt; files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find app/code vendor/&lt;span class="k"&gt;*&lt;/span&gt;/module-&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"events.xml"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  xargs &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"catalog_product_load_after&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;layout_generate_blocks_after"&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pay attention to any observer that does a &lt;code&gt;$this-&amp;gt;_objectManager-&amp;gt;get(...)&lt;/code&gt; or fires a new SQL query. These are red flags.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disabling Modules and Plugins Surgically
&lt;/h2&gt;

&lt;p&gt;If a third-party module registers plugins you don't need, you have two options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1 — Disable a specific plugin in your own module's &lt;code&gt;di.xml&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Catalog\Model\Product"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;plugin&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"ThirdParty_Module::somePlugin"&lt;/span&gt; &lt;span class="na"&gt;disabled=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is clean and upgrade-safe. No core files modified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2 — Disable the entire module:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento module:disable ThirdParty_Module
bin/magento setup:upgrade
bin/magento setup:di:compile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only do this if you've verified the module is truly unused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Offenders in Popular Extensions
&lt;/h2&gt;

&lt;p&gt;From community benchmarks and profiling sessions, these patterns frequently appear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Page builders&lt;/strong&gt; hooking into &lt;code&gt;layout_generate_blocks_after&lt;/code&gt; to inject widgets — can add 30–100ms per page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loyalty / reward point modules&lt;/strong&gt; adding observers on &lt;code&gt;sales_order_place_after&lt;/code&gt; and &lt;code&gt;checkout_cart_product_add_after&lt;/code&gt;, sometimes with synchronous API calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom attribute modules&lt;/strong&gt; stacking &lt;code&gt;around&lt;/code&gt; plugins on &lt;code&gt;getAttribute()&lt;/code&gt; — called dozens of times per product render&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translation/locale plugins&lt;/strong&gt; wrapping &lt;code&gt;__()&lt;/code&gt; — the most-called method in Magento, potentially millions of times per request in heavy catalog pages&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Write Efficient Plugins: A Checklist
&lt;/h2&gt;

&lt;p&gt;When writing your own plugins, follow these rules:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Prefer &lt;code&gt;after&lt;/code&gt; over &lt;code&gt;around&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Only use &lt;code&gt;around&lt;/code&gt; if you need to conditionally prevent the original call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Check the call frequency&lt;/strong&gt;&lt;br&gt;
Before adding a plugin, ask: how many times is this method called per page request? A plugin on &lt;code&gt;Product::getId()&lt;/code&gt; is very different from one on &lt;code&gt;Cart::addProduct()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Guard with early returns&lt;/strong&gt;&lt;br&gt;
If your plugin logic only applies in certain contexts, bail out early:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;afterGetPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Product&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTypeId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'simple'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ... custom logic&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Never do I/O in observers on hot events&lt;/strong&gt;&lt;br&gt;
If you must call an API or run a query, push it to a message queue instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In your observer&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my.topic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Set sort order intentionally&lt;/strong&gt;&lt;br&gt;
Plugins run in sort order. Default is 10. If your plugin must run first or last, set it explicitly. Avoid fighting with other modules' sort orders by profiling the chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Avoid ObjectManager in plugins/observers&lt;/strong&gt;&lt;br&gt;
Injecting via constructor is faster and more cacheable. Using &lt;code&gt;ObjectManager::get()&lt;/code&gt; inside a hot path bypasses DI caching benefits.&lt;/p&gt;
&lt;h2&gt;
  
  
  Measuring Before and After
&lt;/h2&gt;

&lt;p&gt;Before deploying any optimization, establish a baseline. A simple approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Measure with wrk or Apache Bench&lt;/span&gt;
ab &lt;span class="nt"&gt;-n&lt;/span&gt; 100 &lt;span class="nt"&gt;-c&lt;/span&gt; 5 https://yourstore.com/catalog/category/view/id/4

&lt;span class="c"&gt;# Record: Requests per second, mean response time, 99th percentile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After disabling a plugin or observer, re-run the same test. A well-placed optimization on a high-frequency event can yield &lt;strong&gt;50–200ms improvements&lt;/strong&gt; on category and product pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Plugins and observers are not free. Each one adds method calls, stack frames, and potential I/O to your critical path. The Magento ecosystem has hundreds of modules, many of which pile interceptors on the same hot methods.&lt;/p&gt;

&lt;p&gt;The fix isn't to avoid plugins — it's to use them precisely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audit with &lt;code&gt;dev:di:info&lt;/code&gt;&lt;/strong&gt; before adding new plugins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile with Blackfire or Xdebug&lt;/strong&gt; to find actual hot paths&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer &lt;code&gt;after&lt;/code&gt; over &lt;code&gt;around&lt;/code&gt;&lt;/strong&gt; wherever possible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable unused third-party plugins&lt;/strong&gt; surgically via &lt;code&gt;di.xml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push heavy work to queues&lt;/strong&gt; instead of running it synchronously in observers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your users won't see your clever plugin architecture — they'll only feel whether the page loads fast or slow. Make every interceptor earn its place.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Magento 2 Full Page Cache Deep Dive: Hole Punching, Cache Tags, and Invalidation</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Mon, 18 May 2026 09:02:37 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-full-page-cache-deep-dive-hole-punching-cache-tags-and-invalidation-45kg</link>
      <guid>https://dev.to/magevanta/magento-2-full-page-cache-deep-dive-hole-punching-cache-tags-and-invalidation-45kg</guid>
      <description>&lt;p&gt;Magento 2's Full Page Cache (FPC) is one of the most powerful performance tools in your stack — and one of the most misunderstood. Most guides stop at "enable Varnish and configure Redis." But if you want to squeeze every millisecond out of your store, you need to understand what's happening beneath the surface: how pages are cached, what breaks the cache, and how to surgically invalidate only what needs refreshing.&lt;/p&gt;

&lt;p&gt;This post goes deep. By the end you'll understand FPC internals, ESI hole punching, cache tag architecture, and how to build an invalidation strategy that doesn't nuke your entire cache every time a product description changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Magento 2 FPC Actually Works
&lt;/h2&gt;

&lt;p&gt;Magento 2's FPC is built on top of &lt;code&gt;Magento\Framework\App\PageCache&lt;/code&gt;. When a request comes in, Magento checks the FPC storage (either the built-in file/database cache, or Varnish) for a cached response matching that request's context.&lt;/p&gt;

&lt;p&gt;The cache key is composed of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL (including query parameters — carefully)&lt;/li&gt;
&lt;li&gt;Customer group&lt;/li&gt;
&lt;li&gt;Store view&lt;/li&gt;
&lt;li&gt;Currency&lt;/li&gt;
&lt;li&gt;HTTP Vary headers (like &lt;code&gt;X-Magento-Vary&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;X-Magento-Vary&lt;/code&gt; header is a hashed cookie containing the customer's context: logged-in state, group, currency, and any registered context variables. Two visitors with different Vary values get different cache entries — even for the same URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Magento calculates X-Magento-Vary from context data&lt;/span&gt;
&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customerSession&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCustomerGroupId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// + currency, store, etc.&lt;/span&gt;
&lt;span class="nv"&gt;$hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$contextData&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your Vary contexts explode (e.g., too many unique customer groups, personalized data baked into the hash), your cache hit rate plummets. Keep context data minimal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache Tags: The Key to Granular Invalidation
&lt;/h2&gt;

&lt;p&gt;Every cached page response carries &lt;code&gt;X-Magento-Tags&lt;/code&gt; response headers listing all entity tags associated with that response. For example, a product detail page might carry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-Magento-Tags: cat_p,cat_p_1234,cat_c,cat_c_56,cms_b,cms_b_12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These tags say: "this cached page depends on product 1234, category 56, and CMS block 12." When product 1234 is updated, Magento fires a cache invalidation that purges every entry tagged &lt;code&gt;cat_p_1234&lt;/code&gt; — and only those entries.&lt;/p&gt;

&lt;p&gt;This is Magento's &lt;strong&gt;tag-based invalidation&lt;/strong&gt; system, and it's what separates smart cache management from sledgehammer purges.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Tags Are Registered
&lt;/h3&gt;

&lt;p&gt;Cache tags come from &lt;code&gt;Magento\Framework\DataObject\IdentityInterface&lt;/code&gt;. Any block that implements this interface exposes a &lt;code&gt;getIdentities()&lt;/code&gt; method returning its tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractProduct&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;IdentityInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getIdentities&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CACHE_TAG&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getProduct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nc"&gt;Category&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CACHE_TAG&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCategoryId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Magento collects all identities from all blocks rendered on the page and writes them into the &lt;code&gt;X-Magento-Tags&lt;/code&gt; header before storing the cached response. If a third-party extension's blocks don't implement &lt;code&gt;IdentityInterface&lt;/code&gt;, those blocks' dependencies are invisible to the cache — a silent bug that causes stale content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always implement &lt;code&gt;IdentityInterface&lt;/code&gt;&lt;/strong&gt; in custom blocks that render entity-specific data.&lt;/p&gt;

&lt;h2&gt;
  
  
  ESI Hole Punching: Cache the Page, Not the Exceptions
&lt;/h2&gt;

&lt;p&gt;The classic FPC problem: you want to cache the entire page, but some blocks are user-specific (mini-cart, welcome message, wishlist counter). Without hole punching, you either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache the page per-user → cache explodes&lt;/li&gt;
&lt;li&gt;Don't cache the page → no FPC benefit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;ESI (Edge Side Includes)&lt;/strong&gt; solves this. You cache the full page skeleton, but inject ESI tags for the dynamic fragments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;esi:include&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/customer/section/load/?sections=cart,customer"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Varnish (or any ESI-capable reverse proxy) fetches those fragments separately, then assembles the full response. The main page stays cached for all users; only the dynamic fragments are fetched per-session.&lt;/p&gt;

&lt;h3&gt;
  
  
  Magento's Section-Based Approach
&lt;/h3&gt;

&lt;p&gt;Magento 2 doesn't use true ESI for its customer data by default. Instead, it uses a &lt;strong&gt;JavaScript-driven sections system&lt;/strong&gt;. After the initial (cached) page load, the browser makes a lightweight AJAX call to &lt;code&gt;/customer/section/load/&lt;/code&gt; to fetch customer-specific sections (cart, messages, etc.) and hydrates the frontend.&lt;/p&gt;

&lt;p&gt;This means Magento's FPC can cache pages for everyone, then patch in the dynamic data client-side. It's effective but does mean an extra HTTP round-trip on first load. For stores targeting Core Web Vitals, be careful this AJAX call doesn't block LCP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom Private Content Blocks
&lt;/h3&gt;

&lt;p&gt;If you're building a block with user-specific data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyBlock&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractBlock&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;PrivateContentInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getPrivateCacheData&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'ttl'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'my_custom_section'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Implement &lt;code&gt;PrivateContentInterface&lt;/code&gt; and register your section data provider in &lt;code&gt;di.xml&lt;/code&gt;. Magento will handle the AJAX hydration automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache Invalidation Strategies
&lt;/h2&gt;

&lt;p&gt;Bad invalidation strategy = slow responses after any catalog update. Here's how to think about it:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Tag-Based Partial Purge (Preferred)
&lt;/h3&gt;

&lt;p&gt;When an entity is saved, Magento dispatches a &lt;code&gt;clean_cache_by_tags&lt;/code&gt; event. Varnish (via the Magento Varnish config) responds by purging only responses tagged with that entity's cache tag.&lt;/p&gt;

&lt;p&gt;Result: updating product 1234 purges only pages that display product 1234. The rest of your cache is untouched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the default Magento behavior — but it only works properly if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your Varnish &lt;code&gt;default.vcl&lt;/code&gt; includes the tag-based ban logic&lt;/li&gt;
&lt;li&gt;All your custom blocks implement &lt;code&gt;IdentityInterface&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Your invalidation is not being short-circuited by a mis-configured full flush&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Full Cache Flush (Use Sparingly)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;php bin/magento cache:flush full_page&lt;/code&gt; wipes the entire FPC. Appropriate for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Major theme deploys&lt;/li&gt;
&lt;li&gt;Configuration changes that affect all pages&lt;/li&gt;
&lt;li&gt;Emergency stale-content recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Never trigger full flushes for single-entity updates. If your deploy scripts flush the full page cache on every deploy, consider switching to a tag flush of affected entity types instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Scheduled Invalidation via TTL
&lt;/h3&gt;

&lt;p&gt;Every cache entry has a TTL. Magento's default FPC TTL is &lt;strong&gt;86400 seconds (24 hours)&lt;/strong&gt; for public pages. You can tune this in Admin → System → Full Page Cache → TTL.&lt;/p&gt;

&lt;p&gt;For high-traffic stores where content freshness matters, a shorter TTL (e.g., 3600–7200 seconds) with good tag invalidation gives you both freshness and performance. Don't set TTL to 0 — that disables expiration entirely and relies solely on manual invalidation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging FPC Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Check Cache Status Headers
&lt;/h3&gt;

&lt;p&gt;With Varnish, add a &lt;code&gt;Varnish-Cache&lt;/code&gt; or &lt;code&gt;X-Cache&lt;/code&gt; debug header in your VCL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vcl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub&lt;/span&gt; &lt;span class="nf"&gt;vcl_deliver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;obj.hits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;resp.http.X-Cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HIT"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;resp.http.X-Cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MISS"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the built-in FPC, enable &lt;code&gt;developer mode&lt;/code&gt; and watch for &lt;code&gt;X-Magento-Cache-Debug: HIT&lt;/code&gt; / &lt;code&gt;MISS&lt;/code&gt; headers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Cache Miss Culprits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Form keys&lt;/strong&gt; — Magento injects a per-session form key into forms. If your layout renders this server-side in a cacheable block, every request will be a miss. Move form keys to private content or JS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Random content&lt;/strong&gt; — any block that calls &lt;code&gt;rand()&lt;/code&gt; or uses &lt;code&gt;microtime()&lt;/code&gt; will produce a new cache entry every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing Vary alignment&lt;/strong&gt; — if the &lt;code&gt;X-Magento-Vary&lt;/code&gt; cookie changes between requests for the same page (e.g., currency switcher JS), Magento will see each as a unique request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS/HTTP mix&lt;/strong&gt; — always normalise to HTTPS before the cache layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Inspect Tags on Live Pages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://yourstore.com/some-product.html | &lt;span class="nb"&gt;grep &lt;/span&gt;X-Magento-Tags
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the tags list is empty, FPC is not running or this block isn't tagged. If it's enormous (hundreds of tags per page), you may be hitting header size limits — consider compressing tags via a Varnish ban lurker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Varnish Ban Lurker for Large Tag Sets
&lt;/h2&gt;

&lt;p&gt;Magento stores with large catalogs can generate enormous &lt;code&gt;X-Magento-Tags&lt;/code&gt; headers, eventually hitting Varnish's header size limit (default 8KB). The solution: &lt;strong&gt;ban lurker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of purging via tags in the response header, store bans in Varnish's ban list and let the lurker process them asynchronously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vcl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub&lt;/span&gt; &lt;span class="nf"&gt;vcl_recv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"BAN"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;ban&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"obj.http.X-Magento-Tags ~ "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;req.http.X-Magento-Tags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;synth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Banned"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable &lt;code&gt;ban_lurker_sleep&lt;/code&gt; in your Varnish params for the lurker to clean up expired bans efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Checklist
&lt;/h2&gt;

&lt;p&gt;Before calling your FPC setup "production ready":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Varnish VCL matches your Magento version's template&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;X-Magento-Tags&lt;/code&gt; headers visible on cached responses&lt;/li&gt;
&lt;li&gt;[ ] Tag-based purge tested: update a product, confirm only its pages were purged&lt;/li&gt;
&lt;li&gt;[ ] All custom blocks implement &lt;code&gt;IdentityInterface&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Private content blocks use &lt;code&gt;PrivateContentInterface&lt;/code&gt; or JS sections&lt;/li&gt;
&lt;li&gt;[ ] Form keys removed from server-rendered cacheable blocks&lt;/li&gt;
&lt;li&gt;[ ] TTL set to a sensible value (not 0, not 86400 for fast-changing catalogs)&lt;/li&gt;
&lt;li&gt;[ ] FPC hit/miss headers enabled in staging for ongoing debugging&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Magento 2's Full Page Cache is a sophisticated system — not just "cache the whole page and hope for the best." Understanding cache tags, &lt;code&gt;X-Magento-Vary&lt;/code&gt;, ESI hole punching, and granular invalidation gives you a cache that's both fast and accurate.&lt;/p&gt;

&lt;p&gt;The stores that get FPC right aren't the ones that flush everything on every deploy. They're the ones that understand what each page depends on, tag it correctly, and invalidate only what changed. That's the difference between a 90% cache hit rate and a 99% one — and in production traffic, that margin matters enormously.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>caching</category>
    </item>
  </channel>
</rss>
