<?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 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>
    <item>
      <title>Magento 2 Product Import &amp; Export Optimization: Speed Up Large Catalog Operations</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 17 May 2026 09:02:08 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-product-import-export-optimization-speed-up-large-catalog-operations-4p5h</link>
      <guid>https://dev.to/magevanta/magento-2-product-import-export-optimization-speed-up-large-catalog-operations-4p5h</guid>
      <description>&lt;p&gt;If you've ever kicked off a large product import in Magento 2 and watched the progress bar crawl, you know the pain. A 50,000-product CSV that should take minutes ends up running for hours. Exports grind the database to a halt. Scheduled imports time out. It doesn't have to be this way.&lt;/p&gt;

&lt;p&gt;In this guide we'll cover every meaningful lever you can pull to make Magento 2 imports and exports dramatically faster — from server-side tuning to code-level tricks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Magento 2 Imports Are Slow
&lt;/h2&gt;

&lt;p&gt;Before optimizing, it's worth understanding &lt;em&gt;why&lt;/em&gt; imports are slow by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EAV overhead:&lt;/strong&gt; Every product attribute write touches &lt;code&gt;catalog_product_entity_*&lt;/code&gt; tables multiple times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reindex on import:&lt;/strong&gt; By default, Magento triggers reindexing after each batch, not after the full import.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL rewrite generation:&lt;/strong&gt; Each imported product generates URL rewrites — one of the heaviest operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image processing:&lt;/strong&gt; Magento copies and resizes images synchronously during import.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event observers:&lt;/strong&gt; Third-party modules often hook into &lt;code&gt;catalog_product_save_after&lt;/code&gt; and fire on every imported row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database locking:&lt;/strong&gt; Large inserts cause table-level locks that block other queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now let's fix all of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Use the Built-in Import UI Correctly
&lt;/h2&gt;

&lt;p&gt;The Admin import UI (&lt;code&gt;System &amp;gt; Data Transfer &amp;gt; Import&lt;/code&gt;) is fine for occasional imports but has limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Upload limit:&lt;/strong&gt; PHP's &lt;code&gt;upload_max_filesize&lt;/code&gt; and &lt;code&gt;post_max_size&lt;/code&gt; must be increased for large files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout:&lt;/strong&gt; Apache/Nginx timeouts kill long-running imports. Set &lt;code&gt;max_execution_time = 0&lt;/code&gt; in &lt;code&gt;php.ini&lt;/code&gt; for CLI, and configure Nginx timeouts for web uploads:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;fastcgi_read_timeout&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Behavior setting:&lt;/strong&gt; Always use &lt;code&gt;Add/Update&lt;/code&gt; unless you intend to delete. &lt;code&gt;Replace&lt;/code&gt; is much slower because it deletes and re-inserts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For anything above 10,000 products, &lt;strong&gt;use the CLI&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Run Imports via CLI
&lt;/h2&gt;

&lt;p&gt;The CLI import avoids PHP-FPM timeouts entirely:&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 import:run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--entity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;catalog_product &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--behavior&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;add_update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--validation-strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;validation-stop-on-errors &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allowed-error-count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--separator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--multiple-value-separator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--fields-enclosure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /path/to/products.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs in the CLI PHP context where you can set unlimited execution time.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Disable Reindexing During Import
&lt;/h2&gt;

&lt;p&gt;This is the single biggest optimization. Switch all indexes to &lt;code&gt;manual&lt;/code&gt; update mode before importing:&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_product_flat
bin/magento indexer:set-mode schedule catalog_product_price
bin/magento indexer:set-mode schedule catalog_search
bin/magento indexer:set-mode schedule catalogrule_product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the import completes, reindex everything at once and switch back:&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
bin/magento indexer:set-mode realtime catalog_product_flat
&lt;span class="c"&gt;# ... etc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reindexing once after a 50,000-product import is orders of magnitude faster than reindexing after every batch of 100.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Optimize MySQL for Bulk Imports
&lt;/h2&gt;

&lt;p&gt;By default, MySQL is tuned for transactional workloads, not bulk inserts. Temporarily adjust these settings before a large import:&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;innodb_flush_log_at_trx_commit&lt;/span&gt; &lt;span class="o"&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;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;sync_binlog&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;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;innodb_autoinc_lock_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add to &lt;code&gt;my.cnf&lt;/code&gt; for a more permanent performance gain:&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;innodb_buffer_pool_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;4G          # 70-80% of available RAM&lt;/span&gt;
&lt;span class="py"&gt;innodb_log_file_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;512M&lt;/span&gt;
&lt;span class="py"&gt;innodb_flush_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;O_DIRECT&lt;/span&gt;
&lt;span class="py"&gt;innodb_io_capacity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;2000&lt;/span&gt;
&lt;span class="py"&gt;innodb_io_capacity_max&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;4000&lt;/span&gt;
&lt;span class="py"&gt;bulk_insert_buffer_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;64M&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Reset &lt;code&gt;innodb_flush_log_at_trx_commit = 1&lt;/code&gt; in production for durability. Only relax it during controlled import windows.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. Skip URL Rewrites When Possible
&lt;/h2&gt;

&lt;p&gt;URL rewrite generation is one of the most expensive parts of import. If you're doing a backend-only import (products not yet live, or you're updating prices/stock only), disable it:&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;app/etc/env.php&lt;/code&gt;, temporarily set:&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;'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;'frontend'&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;'flat_catalog_product'&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="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or better — import only the columns you need. A CSV with just &lt;code&gt;sku&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, and &lt;code&gt;qty&lt;/code&gt; skips URL rewrite generation entirely because Magento only regenerates rewrites when URL-key fields change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Split your import file into two CSVs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;products_core.csv&lt;/code&gt; — sku, name, description, attributes (triggers URL rewrites)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;products_stock_price.csv&lt;/code&gt; — sku, price, qty only (no URL rewrites)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Import stock/price updates separately and save significant time.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Batch Size Tuning
&lt;/h2&gt;

&lt;p&gt;Magento imports in batches. The default batch size is typically 100–200 rows. Increasing this reduces the number of database round-trips:&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;vendor/magento/module-import-export/Model/Import.php&lt;/code&gt;, the constant &lt;code&gt;DEFAULT_SIZE&lt;/code&gt; controls this — but modifying core is bad practice. Instead, create a plugin:&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;// app/code/Vendor/ImportOptimizer/Plugin/ImportPlugin.php&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;afterGetBunchSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Magento\ImportExport\Model\Import&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;int&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;int&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Increase from default ~100&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test with 500–1000 depending on your row width and available memory. Larger batches reduce overhead but increase memory per batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Disable Non-Essential Observers During Import
&lt;/h2&gt;

&lt;p&gt;Third-party modules frequently attach observers to product save events. During a CLI import, you almost certainly don't need newsletter triggers, ERP sync hooks, or marketing tag updates firing 50,000 times.&lt;/p&gt;

&lt;p&gt;Create a custom CLI command that wraps the import and temporarily disables specific observers, or use Magento's &lt;code&gt;areas&lt;/code&gt; configuration to exclude them from &lt;code&gt;crontab&lt;/code&gt; area context.&lt;/p&gt;

&lt;p&gt;At minimum, identify your heaviest observers:&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 dev:events:list | &lt;span class="nb"&gt;grep &lt;/span&gt;catalog_product_save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check the module list and disable non-critical ones during import windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Image Import Optimization
&lt;/h2&gt;

&lt;p&gt;If your CSV includes images, Magento processes them synchronously — download (if URL), copy to &lt;code&gt;pub/media/catalog/product&lt;/code&gt;, and add to the queue for resizing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practice:&lt;/strong&gt; Import product data first (without images), then import images separately in a second pass. This allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a dedicated import window for image processing&lt;/li&gt;
&lt;li&gt;Run image resize asynchronously via queue consumers&lt;/li&gt;
&lt;li&gt;Retry failed image downloads without re-importing all product data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enable async image resizing:&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/product/flat_catalog_product 0
bin/magento config:set system/upload_configuration/enable_resize_file 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then process the image resize queue separately:&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 queue:consumers:start media.storage.catalog.image.resize
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. Export Optimization
&lt;/h2&gt;

&lt;p&gt;Exports in Magento 2 are often even slower than imports because they join across many EAV tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable flat catalog before large exports:&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 config:set catalog/frontend/flat_catalog_product 1
bin/magento indexer:reindex catalog_product_flat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With flat catalog enabled, exports read from a single denormalized table instead of joining dozens of EAV tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use direct database queries for custom exports:&lt;/strong&gt; If you need a non-standard export (e.g., just SKU + price + stock for an ERP), writing a custom script that queries &lt;code&gt;catalog_product_entity&lt;/code&gt; + &lt;code&gt;cataloginventory_stock_item&lt;/code&gt; directly will always outperform the Magento export framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Monitor and Benchmark
&lt;/h2&gt;

&lt;p&gt;Before and after your optimizations, measure:&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;# Time a full import&lt;/span&gt;
&lt;span class="nb"&gt;time &lt;/span&gt;bin/magento import:run &lt;span class="nt"&gt;--entity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;catalog_product ...

&lt;span class="c"&gt;# Watch MySQL in real-time during import&lt;/span&gt;
mysqladmin &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt; extended-status &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; 1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'Queries|Threads'&lt;/span&gt;

&lt;span class="c"&gt;# Check slow query log&lt;/span&gt;
SET GLOBAL slow_query_log &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;
SET GLOBAL long_query_time &lt;span class="o"&gt;=&lt;/span&gt; 1&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The combination of schedule-mode indexing, MySQL tuning, and disabled URL rewrites on price-only imports typically yields &lt;strong&gt;5–10x speedups&lt;/strong&gt; on large catalogs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together: The Import Checklist
&lt;/h2&gt;

&lt;p&gt;Before running a large import:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Switch relevant indexers to &lt;code&gt;schedule&lt;/code&gt; mode&lt;/li&gt;
&lt;li&gt;[ ] Disable non-essential observers&lt;/li&gt;
&lt;li&gt;[ ] Tune MySQL buffer pool and log settings&lt;/li&gt;
&lt;li&gt;[ ] Split CSV: content vs. price/stock&lt;/li&gt;
&lt;li&gt;[ ] Set &lt;code&gt;max_execution_time = 0&lt;/code&gt; in CLI php.ini&lt;/li&gt;
&lt;li&gt;[ ] Use CLI import, not Admin UI&lt;/li&gt;
&lt;li&gt;[ ] Disable image processing if possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After import:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Run &lt;code&gt;bin/magento indexer:reindex&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Run &lt;code&gt;bin/magento cache:flush&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Switch indexers back to &lt;code&gt;realtime&lt;/code&gt; if desired&lt;/li&gt;
&lt;li&gt;[ ] Process image resize queue&lt;/li&gt;
&lt;li&gt;[ ] Verify product visibility in frontend&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Magento 2 import performance is a layered problem — no single fix solves everything. The biggest wins come from &lt;strong&gt;schedule-mode indexing&lt;/strong&gt; (eliminates mid-import reindex), &lt;strong&gt;MySQL buffer tuning&lt;/strong&gt; (faster bulk inserts), and &lt;strong&gt;splitting CSV files&lt;/strong&gt; by operation type (avoids unnecessary URL rewrite generation).&lt;/p&gt;

&lt;p&gt;Once you've implemented these optimizations, a 50,000-product import that took 3 hours should complete in 15–30 minutes. That's the kind of improvement that makes a real difference in release windows, data migrations, and daily sync pipelines.&lt;/p&gt;

&lt;p&gt;Have a specific import bottleneck you're hitting? The techniques above cover the vast majority of cases — but profiling with &lt;code&gt;EXPLAIN&lt;/code&gt; on the slow queries during import is always a good next step.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 MSI Performance Optimization: Taming Multi-Source Inventory</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sat, 16 May 2026 09:02:25 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-msi-performance-optimization-taming-multi-source-inventory-5a1i</link>
      <guid>https://dev.to/magevanta/magento-2-msi-performance-optimization-taming-multi-source-inventory-5a1i</guid>
      <description>&lt;p&gt;Multi-Source Inventory (MSI) arrived in Magento 2.3 as a major architectural overhaul of stock management. For merchants running multiple warehouses, it's genuinely powerful. For the other 80% — single-warehouse stores that just want fast page loads and quick checkout — it's often an unexpected performance drag that's hard to diagnose and even harder to fix.&lt;/p&gt;

&lt;p&gt;This guide walks through exactly what MSI does under the hood, where the bottlenecks hide, and what you can do about them today.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MSI Actually Does (and Why It's Slow)
&lt;/h2&gt;

&lt;p&gt;Before MSI, Magento's inventory system was a flat, denormalized structure. Stock was stored in &lt;code&gt;cataloginventory_stock_item&lt;/code&gt;, and a simple &lt;code&gt;JOIN&lt;/code&gt; during product listing was all that was needed to determine salability.&lt;/p&gt;

&lt;p&gt;MSI replaced this with a normalized, extensible model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;inventory_source&lt;/code&gt; — physical locations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inventory_source_item&lt;/code&gt; — stock per SKU per source&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inventory_stock&lt;/code&gt; — named stock pools&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inventory_stock_sales_channel&lt;/code&gt; — maps stocks to websites/stores&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inventory_reservation&lt;/code&gt; — deferred (eventual consistency) deductions during checkout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The genius of this design is its flexibility. The performance cost is that &lt;strong&gt;every salability check now requires resolving a chain of tables&lt;/strong&gt; — source → stock → reservation — often via PHP-layer aggregation, not a single SQL query.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;IsSalableConditionInterface&lt;/code&gt; plugin chain and the &lt;code&gt;GetProductSalableQty&lt;/code&gt; service are called on virtually every product load in catalog listing, cart, checkout, and order placement. In a store with thousands of SKUs and a busy queue, this adds up fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosing MSI Overhead
&lt;/h2&gt;

&lt;p&gt;Before optimizing blindly, measure. These are the usual suspects:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Slow Category Pages
&lt;/h3&gt;

&lt;p&gt;Enable the Magento profiler (&lt;code&gt;MAGE_PROFILER=html php bin/magento ...&lt;/code&gt;) or use New Relic / Blackfire and look for repeated calls to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Magento\InventorySalesAdminUi\Model\...
Magento\InventoryCatalog\Plugin\...
Magento\InventoryIndexer\...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see these appearing hundreds of times per page request, MSI is your bottleneck.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Slow Checkout / Add-to-Cart
&lt;/h3&gt;

&lt;p&gt;The reservation mechanism writes to &lt;code&gt;inventory_reservation&lt;/code&gt; on every cart add and order placement. On high-traffic stores, this table grows rapidly and the compensating cron (&lt;code&gt;inventory_cleanup_reservations&lt;/code&gt;) may lag behind.&lt;/p&gt;

&lt;p&gt;Check table size:&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="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;inventory_reservation&lt;/span&gt;&lt;span class="p"&gt;;&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;inventory_stock_1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- adjust stock ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tables with millions of rows indicate the cleanup cron is not keeping up.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Reindex Times
&lt;/h3&gt;

&lt;p&gt;MSI adds several new indexers:&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;inventory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;inventory_stock&lt;/code&gt; reindex can be very slow on large catalogs — especially if it runs in "Update on Save" mode during peak hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization Strategy 1: Switch to Single-Source Mode Correctly
&lt;/h2&gt;

&lt;p&gt;If you're a single-warehouse merchant, MSI is solving problems you don't have. However, you &lt;strong&gt;cannot simply disable the MSI modules&lt;/strong&gt; — they are deeply integrated into Magento's sales flow since 2.3.&lt;/p&gt;

&lt;p&gt;What you &lt;em&gt;can&lt;/em&gt; do is configure MSI to behave like the old single-stock system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use the Default Source and Default Stock only.&lt;/strong&gt; Never create additional sources or stocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set all products to the Default Source.&lt;/strong&gt; If you imported products incorrectly, run:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento inventory:source-items:export-stock-to-default-source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unassign unused sources.&lt;/strong&gt; In Admin → Stores → Sources, disable all non-default sources so Magento skips multi-source resolution logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ensure &lt;code&gt;inventory_stock_sales_channel&lt;/code&gt; maps only the Default Stock to your website.&lt;/strong&gt; Mismatches here cause fallback resolution paths that are significantly slower.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With a clean single-source setup, MSI's internal resolvers hit fast code paths that are barely slower than the old system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization Strategy 2: Indexer Tuning
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Switch to "Update by Schedule"
&lt;/h3&gt;

&lt;p&gt;Real-time (Update on Save) inventory reindex is a common culprit for frontend slowdowns during product edits or imports.&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 inventory_stock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This moves reindexing to cron, keeping the frontend responsive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run Inventory Indexers on Cron Properly
&lt;/h3&gt;

&lt;p&gt;Magento's default cron group for MSI indexers is &lt;code&gt;index&lt;/code&gt;. Verify it's running:&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 cron:status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For high-volume stores, consider running the inventory indexers on a dedicated cron group with a tighter schedule during off-peak hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partial Reindex After Imports
&lt;/h3&gt;

&lt;p&gt;When you import products in bulk, avoid a full &lt;code&gt;inventory_stock&lt;/code&gt; reindex if possible. Use Magento's partial 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 inventory_stock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For very large catalogs, use &lt;code&gt;--no-interaction&lt;/code&gt; with a time limit and offset if you've implemented chunked reindex via a custom CLI command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization Strategy 3: Reservation Table Maintenance
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;inventory_reservation&lt;/code&gt; table uses an &lt;strong&gt;append-only&lt;/strong&gt; model. Magento only cleans it via compensating cron jobs (&lt;code&gt;inventory.reservations.cleanup&lt;/code&gt; and &lt;code&gt;inventory.reservations.updateSalabilityStatus&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;If these lag, the table balloons and reservation resolution becomes slow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check and Force Cleanup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento inventory:reservations:cleanup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For stores processing thousands of orders per day, schedule this to run every 15 minutes instead of the default hourly.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;crontab.xml&lt;/code&gt; of a custom module:&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;job&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"inventory_cleanup_reservations_frequent"&lt;/span&gt; &lt;span class="na"&gt;instance=&lt;/span&gt;&lt;span class="s"&gt;"Magento\InventoryIndexer\Cron\ReindexAfterReservationsPlaced"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"execute"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;schedule&amp;gt;&lt;/span&gt;*/15 * * * *&lt;span class="nt"&gt;&amp;lt;/schedule&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/job&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also run the compensating reservation update more frequently if you see salability showing stale (e.g., item still shows in stock 10 minutes after the last unit sold):&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 inventory:salability:reindex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Archive Old Reservations
&lt;/h3&gt;

&lt;p&gt;For stores that have been running MSI for years without cleanup, a one-time purge of fully compensated reservations is safe:&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;-- Safe: removes reservations where the net quantity per SKU/stock is zero&lt;/span&gt;
&lt;span class="c1"&gt;-- Run this only after verifying with a SELECT first&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;inventory_reservation&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&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;stock_id&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;inventory_reservation&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;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stock_id&lt;/span&gt;
    &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;r2&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r1&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;r2&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;r1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stock_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stock_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;Always run on a staging environment first and take a backup.&lt;/strong&gt; This is a data mutation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization Strategy 4: Salability Check Caching
&lt;/h2&gt;

&lt;p&gt;MSI's &lt;code&gt;GetProductSalableQty&lt;/code&gt; and &lt;code&gt;IsSalable&lt;/code&gt; services are called per-product. On a category page with 48 products, that's 48 individual salability resolutions — each potentially involving multiple DB queries.&lt;/p&gt;

&lt;p&gt;Magento 2.4.4+ introduced a built-in result cache for salability checks. Ensure it's enabled by checking your DI configuration:&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;"IsSalableConditionInterface"&lt;/span&gt; vendor/magento/module-inventory-sales/etc/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're on an older version, you can implement a simple proxy cache using Magento's cache framework:&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 for GetProductSalableQtyInterface&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;aroundExecute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;GetProductSalableQtyInterface&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="nv"&gt;$sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$stockId&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="nv"&gt;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'msi_salable_qty_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$sku&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;$stockId&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="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="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="n"&gt;float&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;$sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$stockId&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="n"&gt;string&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="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 30s TTL&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;Use with care on high-velocity SKUs — a 30-second stale quantity is usually acceptable for catalog display but not for final checkout reservation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization Strategy 5: Disable MSI for Non-Salable Product Types
&lt;/h2&gt;

&lt;p&gt;Configurable products trigger MSI salability checks for every child product to determine if the parent is in stock. For configurables with dozens of variants, this cascades badly.&lt;/p&gt;

&lt;p&gt;Magento provides the &lt;code&gt;InventoryConfigurableProduct&lt;/code&gt; module specifically to aggregate child stock for configurable parents. Ensure this module is enabled:&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:status Magento_InventoryConfigurableProduct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it's disabled (sometimes happens after partial module disablement attempts), re-enable 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 module:enable Magento_InventoryConfigurableProduct
bin/magento setup:upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Monitoring MSI in Production
&lt;/h2&gt;

&lt;p&gt;Set up these dashboards to catch MSI degradation before users notice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New Relic / Datadog custom metrics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;inventory_reservation&lt;/code&gt; row count (alert &amp;gt; 500k)&lt;/li&gt;
&lt;li&gt;Time spent in &lt;code&gt;Magento\InventorySales\Model\IsProductSalableForRequestedQtyCondition&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cron job duration for &lt;code&gt;inventory_cleanup_reservations&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MySQL slow query log:&lt;/strong&gt; Filter for queries against &lt;code&gt;inventory_reservation&lt;/code&gt;, &lt;code&gt;inventory_source_item&lt;/code&gt;, and &lt;code&gt;inventory_stock_*&lt;/code&gt; that exceed 100ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Magento logs:&lt;/strong&gt; Watch &lt;code&gt;var/log/system.log&lt;/code&gt; for &lt;code&gt;Could not resolve salability&lt;/code&gt; or reservation consistency warnings.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Nothing Else Works: Third-Party MSI Alternatives
&lt;/h2&gt;

&lt;p&gt;For stores with extreme inventory complexity (tens of thousands of SKUs, multiple warehouses, real-time 3PL sync), native MSI may genuinely not scale to your needs. Some merchants have had success with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linnworks&lt;/strong&gt; or &lt;strong&gt;SkuVault&lt;/strong&gt; with a Magento connector that bypasses MSI entirely and writes directly to the legacy &lt;code&gt;cataloginventory_stock_item&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom &lt;code&gt;Magento_InventoryCatalog&lt;/code&gt; preference&lt;/strong&gt; that replaces the salability check with a direct single-table lookup for single-source setups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The latter approach (essentially shunting around MSI's resolution chain while keeping the module structure intact for compatibility) is the most maintainable path if you need single-source speed with MSI's API compatibility.&lt;/p&gt;

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

&lt;p&gt;MSI is not inherently slow — it's complex, and complexity has a cost. The optimization playbook:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&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;Slow category pages&lt;/td&gt;
&lt;td&gt;Single-source cleanup + salability caching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow checkout / cart&lt;/td&gt;
&lt;td&gt;Reservation table maintenance + frequent cleanup cron&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow reindex&lt;/td&gt;
&lt;td&gt;Switch to schedule mode + partial reindex on imports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stale stock display&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;inventory:salability:reindex&lt;/code&gt; more frequently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;General MSI overhead&lt;/td&gt;
&lt;td&gt;Ensure &lt;code&gt;InventoryConfigurableProduct&lt;/code&gt; is enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start with the reservation table — it's the most common and most impactful quick win. Then profile to find your specific bottleneck before diving into deeper changes.&lt;/p&gt;

&lt;p&gt;Got a specific MSI issue that isn't covered here? Drop a comment or reach out — real-world MSI war stories are always educational.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Database Deadlocks: Causes, Detection &amp; Prevention</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Fri, 15 May 2026 09:02:23 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-database-deadlocks-causes-detection-prevention-1856</link>
      <guid>https://dev.to/magevanta/magento-2-database-deadlocks-causes-detection-prevention-1856</guid>
      <description>&lt;p&gt;Database deadlocks are one of those production issues that don't announce themselves with a flashy error page — they hide in your MySQL slow query log, surface as mysterious 500 errors during peak traffic, and leave your team scratching their heads at 2 AM. If you're running Magento 2 at scale, deadlocks are not a question of &lt;em&gt;if&lt;/em&gt;, but &lt;em&gt;when&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This guide covers the full picture: why deadlocks happen in Magento specifically, how to detect them before they cause real damage, and concrete prevention strategies you can implement today.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Database Deadlock?
&lt;/h2&gt;

&lt;p&gt;A deadlock occurs when two or more transactions are each waiting for the other to release a lock, creating a circular dependency that can never resolve on its own. MySQL's InnoDB engine detects this situation and automatically rolls back one of the transactions — the "victim" — returning a &lt;code&gt;Deadlock found when trying to get lock; try restarting transaction&lt;/code&gt; error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; 
try restarting transaction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Magento will log these and, if retry logic is in place, silently retry. But under high concurrency, deadlocks stack up fast and degrade the entire checkout flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Magento 2 Is Especially Prone to Deadlocks
&lt;/h2&gt;

&lt;p&gt;Magento's architecture involves several high-concurrency write patterns that are classic deadlock recipes:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Inventory Reservation During Checkout
&lt;/h3&gt;

&lt;p&gt;When multiple customers checkout concurrently with overlapping cart items, Magento locks inventory rows in &lt;code&gt;inventory_reservation&lt;/code&gt; and &lt;code&gt;cataloginventory_stock_item&lt;/code&gt;. If two transactions lock the rows in different orders — which Magento's parallel processing easily triggers — you get a deadlock.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Quote and Order Tables
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;quote&lt;/code&gt;, &lt;code&gt;quote_item&lt;/code&gt;, &lt;code&gt;sales_order&lt;/code&gt;, and &lt;code&gt;sales_order_item&lt;/code&gt; tables are constantly written during the checkout flow. Magento updates totals, applies rules, reserves stock, and generates orders — all in heavily nested transactions. These tables see high lock contention during flash sales or email campaigns.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. EAV Attribute Updates
&lt;/h3&gt;

&lt;p&gt;The EAV tables (&lt;code&gt;catalog_product_entity_*&lt;/code&gt;, &lt;code&gt;customer_entity_*&lt;/code&gt;) use multi-row inserts and updates. Under concurrent import or mass update jobs running alongside regular traffic, these tables frequently deadlock.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Indexer Runs During Business Hours
&lt;/h3&gt;

&lt;p&gt;Running indexers (especially &lt;code&gt;catalog_product_price&lt;/code&gt; or &lt;code&gt;catalogrule_rule&lt;/code&gt;) while the storefront is serving traffic creates massive lock contention. Indexers can lock entire index tables while customer-facing queries try to read them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting Deadlocks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Check the InnoDB Status
&lt;/h3&gt;

&lt;p&gt;The most direct way to see recent deadlocks:&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="n"&gt;ENGINE&lt;/span&gt; &lt;span class="n"&gt;INNODB&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="k"&gt;G&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the &lt;code&gt;LATEST DETECTED DEADLOCK&lt;/code&gt; section. It will show you the exact transactions involved, which tables and rows were locked, and which transaction was rolled back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enable the InnoDB Deadlock Log
&lt;/h3&gt;

&lt;p&gt;For persistent logging, add this to your &lt;code&gt;my.cnf&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="nn"&gt;[mysqld]&lt;/span&gt;
&lt;span class="py"&gt;innodb_print_all_deadlocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ON&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This writes every deadlock to the MySQL error log (&lt;code&gt;/var/log/mysql/error.log&lt;/code&gt;), giving you a historical record to analyze patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitor with Performance Schema
&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;performance_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events_errors_summary_global_by_error&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;error_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ER_LOCK_DEADLOCK'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows the cumulative deadlock count since MySQL started — useful for baselining and alerting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Magento Exception Log
&lt;/h3&gt;

&lt;p&gt;Deadlocks that Magento doesn't retry successfully will end up in &lt;code&gt;var/log/exception.log&lt;/code&gt;. Filter for &lt;code&gt;1213&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="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"1213"&lt;/span&gt; var/log/exception.log | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're seeing more than a handful per hour during peak traffic, you have a real problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevention Strategies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Reduce Transaction Scope
&lt;/h3&gt;

&lt;p&gt;The longer a transaction holds locks, the higher the chance of a deadlock. Review custom code and plugins that wrap large operations in single transactions. Split them into smaller, targeted transactions where possible.&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;// Bad: one big transaction&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;transactionFactory&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$product&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;addObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$stockItem&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;addObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$priceRule&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;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Better: separate, focused saves&lt;/span&gt;
&lt;span class="nv"&gt;$product&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;$stockItem&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Consistent Lock Ordering
&lt;/h3&gt;

&lt;p&gt;Deadlocks often happen because two transactions acquire the same locks in different orders. If you have custom code that locks multiple rows or tables, ensure all code paths always acquire locks in the same order (e.g., always lock by entity ID ascending).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; Sparingly
&lt;/h3&gt;

&lt;p&gt;Magento (and many third-party modules) overuse &lt;code&gt;SELECT FOR UPDATE&lt;/code&gt;. This pessimistic locking is often unnecessary. Consider whether optimistic locking — check-then-update with a version column — is sufficient for your use case.&lt;/p&gt;

&lt;p&gt;For inventory specifically, Magento 2.3+ introduced the &lt;strong&gt;Inventory Reservation&lt;/strong&gt; pattern (&lt;code&gt;inventory_reservation&lt;/code&gt; table with append-only inserts) precisely to reduce lock contention. Make sure you're on Magento 2.3+ MSI and not using legacy &lt;code&gt;CatalogInventory&lt;/code&gt; where avoidable.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Isolate Indexer Runs
&lt;/h3&gt;

&lt;p&gt;Schedule all indexers to run during off-peak hours. Even better, switch from full reindex to &lt;strong&gt;incremental (realtime) indexing&lt;/strong&gt; for most indexers — this spreads the write load over time instead of creating a burst:&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 indexer:set-mode schedule catalog_product_price catalogrule_rule
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For indexers that must run in batch, use a maintenance window and disable your load balancer from sending traffic during that period.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Tune InnoDB Lock Wait Timeout
&lt;/h3&gt;

&lt;p&gt;By default, InnoDB waits 50 seconds before giving up on a lock. That's too long for a web request. Tune it down:&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="nn"&gt;[mysqld]&lt;/span&gt;
&lt;span class="py"&gt;innodb_lock_wait_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This causes deadlock victims to fail faster, reducing the cascade effect on your web tier. Magento's retry logic will handle most of these gracefully.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Switch to READ COMMITTED Isolation
&lt;/h3&gt;

&lt;p&gt;InnoDB's &lt;code&gt;REPEATABLE READ&lt;/code&gt; isolation level (Magento's default) uses gap locks, which increase deadlock risk. Switching to &lt;code&gt;READ COMMITTED&lt;/code&gt; reduces gap locking:&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="nn"&gt;[mysqld]&lt;/span&gt;
&lt;span class="py"&gt;transaction_isolation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;READ-COMMITTED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is safe for most Magento workloads and is often recommended in high-traffic setups. Test thoroughly in staging first — some edge cases in custom code may rely on the stricter isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Connection Pooling
&lt;/h3&gt;

&lt;p&gt;Too many simultaneous database connections increase the probability of lock contention. Use a connection pooler like &lt;strong&gt;ProxySQL&lt;/strong&gt; or tune &lt;code&gt;max_connections&lt;/code&gt; alongside PHP-FPM pool sizes to prevent connection storms during traffic spikes. See our &lt;a href="https://dev.to/blog/magento-2-database-connection-pooling"&gt;Database Connection Pooling guide&lt;/a&gt; for details.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Audit Third-Party Modules
&lt;/h3&gt;

&lt;p&gt;Many deadlocks originate in poorly written third-party modules that use direct SQL writes, lock entire tables, or run in hooks that fire during transactions. Use the InnoDB status output to identify which tables are involved in your deadlocks, then audit which modules write to those tables.&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 tables with high lock wait counts&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;object_schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;object_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count_read_with_shared_locks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;count_write_allow_write&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sum_timer_wait&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;performance_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_lock_waits_summary_by_table&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;sum_timer_wait&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling Deadlocks in Custom Code
&lt;/h2&gt;

&lt;p&gt;If you're building custom functionality that writes to Magento's core tables, implement retry logic:&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;$maxRetries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$attempt&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;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$maxRetries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&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;doDatabaseWork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;\Zend_Db_Statement_Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&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="nf"&gt;str_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'1213'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$maxRetries&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;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nb"&gt;usleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$attempt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// exponential backoff&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nv"&gt;$e&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;Exponential backoff is important — retrying immediately often just causes another deadlock with the same competing transaction.&lt;/p&gt;

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

&lt;p&gt;Deadlocks in Magento 2 are manageable once you know where to look. The key takeaways:&lt;/p&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Enable &lt;code&gt;innodb_print_all_deadlocks&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Visibility into deadlock patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schedule indexers off-peak&lt;/td&gt;
&lt;td&gt;Reduces lock contention significantly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Switch indexers to &lt;code&gt;schedule&lt;/code&gt; mode&lt;/td&gt;
&lt;td&gt;Spreads write load over time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set &lt;code&gt;transaction_isolation = READ-COMMITTED&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Reduces gap locks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tune &lt;code&gt;innodb_lock_wait_timeout = 10&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Faster failure, less cascade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit third-party modules&lt;/td&gt;
&lt;td&gt;Often the root cause&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start with visibility — enable logging, identify your most frequent deadlock tables, then apply targeted fixes. In most cases, a combination of indexer scheduling and isolation level tuning resolves 80% of the deadlock volume. For the remainder, it's usually a specific module or custom code path that needs attention.&lt;/p&gt;

&lt;p&gt;Don't let deadlocks silently degrade your checkout. The data is all there in MySQL — you just need to look.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>mysql</category>
      <category>performance</category>
      <category>php</category>
    </item>
    <item>
      <title>Hyvä Theme Performance: Why You Should Ditch Luma in 2026</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Thu, 14 May 2026 09:02:55 +0000</pubDate>
      <link>https://dev.to/magevanta/hyva-theme-performance-why-you-should-ditch-luma-in-2026-2l4n</link>
      <guid>https://dev.to/magevanta/hyva-theme-performance-why-you-should-ditch-luma-in-2026-2l4n</guid>
      <description>&lt;p&gt;If you're still running a Luma-based theme on your Magento 2 store in 2026, you're leaving significant performance on the table. The Hyvä theme has matured into the de-facto frontend standard for Magento developers who care about speed, maintainability, and Core Web Vitals. This guide explains exactly why, with numbers to back it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Luma
&lt;/h2&gt;

&lt;p&gt;Luma shipped with Magento 2.0 in 2015. It was designed for an era before Lighthouse scores, INP metrics, or the performance bar Google sets today. Under the hood, Luma relies on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RequireJS&lt;/strong&gt; for module loading — hundreds of individual JS requests (or a slow bundled blob)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KnockoutJS&lt;/strong&gt; for UI components — a heavy runtime that initialises on every page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;jQuery&lt;/strong&gt; and dozens of jQuery plugins&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSS compiled from deep LESS inheritance chains&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result? A typical Luma homepage ships with &lt;strong&gt;400–600 KB of JavaScript&lt;/strong&gt; that must parse and execute before anything interactive works. On a fast connection and a desktop Chrome, this is tolerable. On mobile — which accounts for 60%+ of e-commerce traffic — it's a conversion killer.&lt;/p&gt;

&lt;p&gt;Typical Lighthouse scores for a reasonably optimised Luma store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance:&lt;/strong&gt; 40–60 (mobile)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LCP:&lt;/strong&gt; 4–7 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TBT (Total Blocking Time):&lt;/strong&gt; 1,500–3,000 ms&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Hyvä Does Differently
&lt;/h2&gt;

&lt;p&gt;Hyvä, built by Willem Wigman and the community around it, throws out the RequireJS/KnockoutJS stack entirely and replaces it with two modern, lightweight tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alpine.js&lt;/strong&gt; — ~15 KB gzipped, declarative, zero build step for simple interactions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; — utility-first, purged to only the classes you actually use (typically 10–30 KB)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No RequireJS. No KnockoutJS. No jQuery (unless a third-party module forces it). The JavaScript bundle shrinks from 400+ KB to &lt;strong&gt;under 40 KB&lt;/strong&gt; in a clean Hyvä setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Architecture Differences
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Luma&lt;/th&gt;
&lt;th&gt;Hyvä&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JS Framework&lt;/td&gt;
&lt;td&gt;KnockoutJS + RequireJS&lt;/td&gt;
&lt;td&gt;Alpine.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS&lt;/td&gt;
&lt;td&gt;LESS (compiled)&lt;/td&gt;
&lt;td&gt;Tailwind CSS (purged)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JS bundle size&lt;/td&gt;
&lt;td&gt;~400–600 KB&lt;/td&gt;
&lt;td&gt;~30–50 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS size&lt;/td&gt;
&lt;td&gt;~200–400 KB&lt;/td&gt;
&lt;td&gt;~10–30 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component system&lt;/td&gt;
&lt;td&gt;XML layout + phtml + JS mixins&lt;/td&gt;
&lt;td&gt;phtml + Alpine data attributes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build tooling&lt;/td&gt;
&lt;td&gt;Grunt / &lt;code&gt;bin/magento setup:static-content:deploy&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Vite (optional) + Tailwind CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Real-World Performance Numbers
&lt;/h2&gt;

&lt;p&gt;Community benchmarks and case studies consistently show the same pattern. Here's what you can expect after a Hyvä migration on a mid-size Magento store:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile Lighthouse (representative averages):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance score: &lt;strong&gt;75–95&lt;/strong&gt; (up from 40–60)&lt;/li&gt;
&lt;li&gt;LCP: &lt;strong&gt;1.5–2.5 s&lt;/strong&gt; (down from 4–7 s)&lt;/li&gt;
&lt;li&gt;TBT: &lt;strong&gt;100–300 ms&lt;/strong&gt; (down from 1,500–3,000 ms)&lt;/li&gt;
&lt;li&gt;CLS: &lt;strong&gt;&amp;lt; 0.05&lt;/strong&gt; (Hyvä's server-rendered approach avoids layout shift)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't cherry-picked numbers. They're the range you see across e-commerce blogs, Hyvä's own case studies, and agencies that have done dozens of migrations.&lt;/p&gt;

&lt;p&gt;Google's Core Web Vitals directly influence organic ranking. Improving your mobile LCP from 5 seconds to 2 seconds is not just a UX win — it's an SEO win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static Content Deploy: A Hidden Bonus
&lt;/h2&gt;

&lt;p&gt;One pain that every Magento 2 team lives with is &lt;code&gt;setup:static-content:deploy&lt;/code&gt;. On a Luma store with multiple themes and locales, this command can take &lt;strong&gt;10–20 minutes&lt;/strong&gt;. This blocks deployment pipelines, makes hotfixes painful, and slows CI/CD cycles.&lt;/p&gt;

&lt;p&gt;Hyvä's CSS is generated by Tailwind's CLI in &lt;strong&gt;under 10 seconds&lt;/strong&gt;, even for large stores. The JS doesn't need RequireJS bundling at all. Your total static asset build time drops dramatically.&lt;/p&gt;

&lt;p&gt;For teams using zero-downtime deployment strategies (Blue/Green, symlink swaps), this shorter build time means less risk and faster rollbacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About Third-Party Extensions?
&lt;/h2&gt;

&lt;p&gt;This is the real question every merchant asks. Most Magento 2 extensions ship with Luma frontend components. When you switch to Hyvä, those KnockoutJS/RequireJS components simply don't exist.&lt;/p&gt;

&lt;p&gt;The Hyvä ecosystem has addressed this through the &lt;strong&gt;Hyvä Compatibility Layer&lt;/strong&gt; (a paid add-on module) which allows Luma UI components to run inside a Hyvä page as an iframe fallback. It's not pretty, but it works as a bridge.&lt;/p&gt;

&lt;p&gt;More importantly: the major extensions — Amasty, Mageworx, Mollie, Klarna, Stripe, Yotpo, and many others — now ship dedicated Hyvä compatibility modules, either bundled or via the Hyvä Marketplace. Before migrating, audit your extension stack:&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;# List all installed third-party modules&lt;/span&gt;
bin/magento module:status | &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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;Does it have a Hyvä-compatible module on the Hyvä Marketplace?&lt;/li&gt;
&lt;li&gt;Does the vendor provide their own Hyvä support?&lt;/li&gt;
&lt;li&gt;Can the functionality be replaced with a Hyvä-native extension?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Migration Strategy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Full Migration (Greenfield)
&lt;/h3&gt;

&lt;p&gt;Best for stores that are redesigning anyway or have simple extension stacks. You build a new Hyvä theme from scratch, implement all frontend templates in phtml + Alpine, and migrate one page type at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeline:&lt;/strong&gt; 4–12 weeks depending on store complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Progressive Migration
&lt;/h3&gt;

&lt;p&gt;Start with Hyvä as the base theme but keep Luma fallback for complex checkout or account pages using the compatibility layer. Migrate page types incrementally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeline:&lt;/strong&gt; 2–4 weeks for initial launch, then ongoing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Hyvä + React Checkout
&lt;/h3&gt;

&lt;p&gt;Pairing Hyvä with a React-based checkout (Checkout Suite, Hyva Checkout, or React Checkout for Magento) gives you maximum flexibility for the most performance-critical page. Many agencies now recommend this as the default stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up a Basic Hyvä Theme
&lt;/h2&gt;

&lt;p&gt;Hyvä is a paid theme (one-time license ~€1,000 for the base theme). After purchase you get access to the private Composer repository.&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;# Add Hyvä repo to your composer.json&lt;/span&gt;
composer config repositories.hyva-themes composer https://hyva-themes.gitlab.io/magento2-hyva-compat/packages.json

&lt;span class="c"&gt;# Install&lt;/span&gt;
composer require hyva-themes/magento2-default-theme

&lt;span class="c"&gt;# Create your child theme&lt;/span&gt;
bin/magento hyva:config:generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your child theme lives in &lt;code&gt;app/design/frontend/YourVendor/YourTheme/&lt;/code&gt; and follows standard Magento theme inheritance. You override only the templates you need; everything else inherits from the default Hyvä theme.&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;!-- theme.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;theme&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
       &lt;span class="na"&gt;xsi:noNamespaceSchemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"urn:magento:framework:Config/etc/theme.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Your Store Theme&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;parent&amp;gt;&lt;/span&gt;Hyva/default&lt;span class="nt"&gt;&amp;lt;/parent&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/theme&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tailwind Configuration for Magento
&lt;/h2&gt;

&lt;p&gt;Hyvä ships with a &lt;code&gt;tailwind.config.js&lt;/code&gt; that scans your phtml files for class names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tailwind.config.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/**/*.phtml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/**/*.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Include Hyvä default theme templates too&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../vendor/hyva-themes/magento2-default-theme/Magento_Theme/templates/**/*.phtml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;lighter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f0f9ff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;DEFAULT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#0ea5e9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;darker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#0369a1&lt;/span&gt;&lt;span class="dl"&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;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;Build your CSS:&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;# Development (watch mode)&lt;/span&gt;
npx tailwindcss &lt;span class="nt"&gt;-i&lt;/span&gt; ./src/css/styles.css &lt;span class="nt"&gt;-o&lt;/span&gt; ./web/css/styles.css &lt;span class="nt"&gt;--watch&lt;/span&gt;

&lt;span class="c"&gt;# Production (minified + purged)&lt;/span&gt;
&lt;span class="nv"&gt;NODE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production npx tailwindcss &lt;span class="nt"&gt;-i&lt;/span&gt; ./src/css/styles.css &lt;span class="nt"&gt;-o&lt;/span&gt; ./web/css/styles.css &lt;span class="nt"&gt;--minify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Alpine.js Components in Hyvä
&lt;/h2&gt;

&lt;p&gt;Hyvä uses Alpine.js for interactive components. Here's a simple example — a custom product quantity selector:&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;div&lt;/span&gt; &lt;span class="na"&gt;x-data=&lt;/span&gt;&lt;span class="s"&gt;"{ qty: 1, min: 1, max: 100 }"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center gap-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
        &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-8 h-8 border rounded"&lt;/span&gt;
        &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"qty = Math.max(min, qty - 1)"&lt;/span&gt;
        &lt;span class="na"&gt;:disabled=&lt;/span&gt;&lt;span class="s"&gt;"qty &amp;lt;= min"&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;−&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
        &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt;
        &lt;span class="na"&gt;x-model.number=&lt;/span&gt;&lt;span class="s"&gt;"qty"&lt;/span&gt;
        &lt;span class="na"&gt;:min=&lt;/span&gt;&lt;span class="s"&gt;"min"&lt;/span&gt;
        &lt;span class="na"&gt;:max=&lt;/span&gt;&lt;span class="s"&gt;"max"&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-16 text-center border rounded"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
        &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-8 h-8 border rounded"&lt;/span&gt;
        &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"qty = Math.min(max, qty + 1)"&lt;/span&gt;
        &lt;span class="na"&gt;:disabled=&lt;/span&gt;&lt;span class="s"&gt;"qty &amp;gt;= max"&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No KnockoutJS data-bind syntax. No RequireJS define(). Just HTML attributes that any developer can read and understand immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Migrate?
&lt;/h2&gt;

&lt;p&gt;If your store is under active development and you're planning a redesign, &lt;strong&gt;yes, migrate to Hyvä&lt;/strong&gt;. The performance gains are real, the developer experience is dramatically better, and the community has reached critical mass.&lt;/p&gt;

&lt;p&gt;If you're running a stable store with no upcoming redesign and 20+ complex extensions, a migration needs careful planning. The ROI is there — better Core Web Vitals, faster deploys, happier developers — but the effort is real.&lt;/p&gt;

&lt;p&gt;As a rule of thumb: any new Magento 2 project in 2026 should start with Hyvä by default. Luma is legacy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.hyva.io" rel="noopener noreferrer"&gt;Hyvä official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hyva.io/hyva-marketplace.html" rel="noopener noreferrer"&gt;Hyvä Marketplace&lt;/a&gt; — compatible extensions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hyva.io/slack" rel="noopener noreferrer"&gt;Hyvä Community Slack&lt;/a&gt; — active community&lt;/li&gt;
&lt;li&gt;&lt;a href="https://alpinejs.dev" rel="noopener noreferrer"&gt;Alpine.js documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailwindcss.com/docs" rel="noopener noreferrer"&gt;Tailwind CSS documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frontend world has moved on from 2015-era JavaScript frameworks. Hyvä is how Magento 2 keeps up.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>performance</category>
      <category>php</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Magento 2 Session Storage Optimization: Redis, Files, and Database Sessions Compared</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Wed, 13 May 2026 15:31:56 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-session-storage-optimization-redis-files-and-database-sessions-compared-3f4b</link>
      <guid>https://dev.to/magevanta/magento-2-session-storage-optimization-redis-files-and-database-sessions-compared-3f4b</guid>
      <description>&lt;p&gt;Session management is one of those quiet performance killers that rarely gets attention until your store starts grinding under load. By default, Magento 2 stores sessions on the filesystem — a setup that works fine for a single-server development environment but falls apart fast in production. In this guide, we'll break down every viable session storage option, benchmark the trade-offs, and show you exactly how to configure Redis sessions the right way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Session Storage Matters
&lt;/h2&gt;

&lt;p&gt;Every visitor to your Magento store — logged-in or guest — gets a session. That session tracks their cart, wishlist, recently viewed products, and authentication state. On a busy store handling thousands of concurrent visitors, the overhead of reading and writing session data adds up fast.&lt;/p&gt;

&lt;p&gt;The three main options are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Filesystem sessions&lt;/strong&gt; — the default, stored in &lt;code&gt;var/session/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database sessions&lt;/strong&gt; — stored in the &lt;code&gt;session&lt;/code&gt; table in MySQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis sessions&lt;/strong&gt; — stored in memory via a dedicated Redis instance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's go through each in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filesystem Sessions
&lt;/h2&gt;

&lt;p&gt;This is what you get out of the box. Magento writes a file per session to &lt;code&gt;var/session/&lt;/code&gt; using PHP's built-in file session handler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero configuration&lt;/li&gt;
&lt;li&gt;Works immediately with no dependencies&lt;/li&gt;
&lt;li&gt;Easy to inspect for debugging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does not scale to multiple web nodes (sessions are local to one server)&lt;/li&gt;
&lt;li&gt;File system I/O becomes a bottleneck under load&lt;/li&gt;
&lt;li&gt;No built-in expiration management — old session files accumulate and must be cleaned with cron&lt;/li&gt;
&lt;li&gt;Prone to race conditions under concurrent requests from the same session&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a single-server setup with moderate traffic (under ~100 concurrent users), filesystem sessions are acceptable. Beyond that, you're leaving performance on the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database Sessions
&lt;/h2&gt;

&lt;p&gt;Magento 2 supports storing sessions in the MySQL &lt;code&gt;session&lt;/code&gt; table by setting the session save handler to &lt;code&gt;db&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;// app/etc/env.php&lt;/span&gt;
&lt;span class="s1"&gt;'session'&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;'save'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'db'&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;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works across multiple web nodes&lt;/li&gt;
&lt;li&gt;Data is persistent across server restarts&lt;/li&gt;
&lt;li&gt;Session locking is handled correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adds load to your MySQL server — often the most constrained resource&lt;/li&gt;
&lt;li&gt;Table grows without bound; requires aggressive cleanup&lt;/li&gt;
&lt;li&gt;Much slower than Redis for read/write operations (disk I/O vs memory)&lt;/li&gt;
&lt;li&gt;Locking overhead on high-concurrency stores&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Database sessions are a step up from filesystem if you need multi-server support but can't run Redis. In practice, they're rarely the right long-term choice because they shift bottleneck load onto MySQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redis Sessions — The Right Choice for Production
&lt;/h2&gt;

&lt;p&gt;Redis is an in-memory data store, and it's the recommended session backend for any Magento 2 production environment. Magento ships with a built-in Redis session handler (&lt;code&gt;Cm_RedisSession&lt;/code&gt;) from Magento 2.0.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Redis Wins
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed:&lt;/strong&gt; Memory operations are orders of magnitude faster than disk or database I/O&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic expiry:&lt;/strong&gt; TTL-based expiration means no manual session cleanup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-node safe:&lt;/strong&gt; All web servers share the same session store&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency handling:&lt;/strong&gt; Built-in advisory locking prevents session corruption&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring:&lt;/strong&gt; Redis CLI and tools like RedisInsight give full visibility into session data&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Benchmark Context
&lt;/h3&gt;

&lt;p&gt;On a typical Magento 2 store under load test (500 concurrent users, mixed cart/browse traffic):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Avg. session read (ms)&lt;/th&gt;
&lt;th&gt;Avg. session write (ms)&lt;/th&gt;
&lt;th&gt;Server CPU impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Filesystem&lt;/td&gt;
&lt;td&gt;8–15&lt;/td&gt;
&lt;td&gt;12–20&lt;/td&gt;
&lt;td&gt;Low (disk I/O)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL DB&lt;/td&gt;
&lt;td&gt;15–40&lt;/td&gt;
&lt;td&gt;20–50&lt;/td&gt;
&lt;td&gt;High (query load)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;td&gt;0.3–1&lt;/td&gt;
&lt;td&gt;0.5–1.5&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These numbers vary by hardware, but the pattern is consistent: Redis session I/O is 10–50x faster than alternatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring Redis Sessions in Magento 2
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Redis installed and running (typically on port 6379)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;separate Redis instance or database number&lt;/strong&gt; for sessions vs. full page cache (important — see below)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Basic Configuration
&lt;/h3&gt;

&lt;p&gt;Edit &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;'session'&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;'save'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'redis'&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;'host'&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;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6379'&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="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2.5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'persistent_identifier'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'compression_threshold'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2048'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'compression_library'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'gzip'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'log_level'&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;span class="s1"&gt;'max_concurrency'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'break_after_frontend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'break_after_adminhtml'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'30'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'first_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'600'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'bot_first_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'60'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'bot_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'7200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'disable_locking'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'min_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'60'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'max_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2592000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'sentinel_master'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'sentinel_servers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'sentinel_connect_retries'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'sentinel_verify_master'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'0'&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;h3&gt;
  
  
  Key Parameters Explained
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;database&lt;/code&gt;&lt;/strong&gt; — Use a different number (0, 1, 2...) than your default cache and full page cache Redis instances. Mixing them makes TTL and memory management a nightmare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;max_concurrency&lt;/code&gt;&lt;/strong&gt; — Maximum number of processes that can wait for a session lock at once. The default of 6 is fine for most stores. Lower it on smaller servers to avoid Redis connection exhaustion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;break_after_frontend&lt;/code&gt;&lt;/strong&gt; — Seconds to wait before breaking a session lock on the frontend. A value of 5 is aggressive but prevents hung requests from blocking the cart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;disable_locking&lt;/code&gt;&lt;/strong&gt; — Leave this at &lt;code&gt;0&lt;/code&gt;. Disabling session locking speeds things up but causes data corruption on concurrent AJAX requests (e.g., add-to-cart + minicart update firing simultaneously).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;compression_threshold&lt;/code&gt;&lt;/strong&gt; — Session data over this size in bytes gets compressed. Reduces Redis memory usage for stores with large cart data or extensive customer session payloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;bot_first_lifetime&lt;/code&gt; / &lt;code&gt;bot_lifetime&lt;/code&gt;&lt;/strong&gt; — Short TTLs for detected bots. This alone can dramatically reduce Redis memory usage on high-traffic stores where crawlers represent 20–40% of sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separating Sessions from Cache in Redis
&lt;/h2&gt;

&lt;p&gt;A common mistake is pointing both &lt;code&gt;cache&lt;/code&gt; and &lt;code&gt;session&lt;/code&gt; at the same Redis instance with the same database number. This causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sessions and cached HTML blocks competing for memory&lt;/li&gt;
&lt;li&gt;When Redis hits &lt;code&gt;maxmemory&lt;/code&gt;, eviction policies may delete session data&lt;/li&gt;
&lt;li&gt;Harder to tune TTLs independently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Recommended setup:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Redis DB 0 → Default cache (blocks, collections, config)
Redis DB 1 → Full Page Cache
Redis DB 2 → Sessions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use separate Redis ports/instances if your server has the memory to support it. For session storage, configure &lt;code&gt;maxmemory-policy noeviction&lt;/code&gt; — you never want Redis silently dropping sessions because it ran out of memory. Size your Redis instance accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redis Sentinel for High Availability
&lt;/h2&gt;

&lt;p&gt;If your store needs zero-downtime session persistence, Redis Sentinel provides automatic failover. Magento 2 supports Sentinel natively through the same &lt;code&gt;env.php&lt;/code&gt; configuration:&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;'sentinel_master'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'mymaster'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'sentinel_servers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'192.168.1.10:26379,192.168.1.11:26379,192.168.1.12:26379'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For most Magento stores, a single Redis instance with persistence (&lt;code&gt;appendonly yes&lt;/code&gt; in &lt;code&gt;redis.conf&lt;/code&gt;) is sufficient. Sentinel adds operational complexity that's only worth it at significant scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session Lifetime Tuning
&lt;/h2&gt;

&lt;p&gt;Session lifetime directly affects how long customers stay "logged in" and how much memory Redis consumes.&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;'max_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2592000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 30 days — good for B2C&lt;/span&gt;
&lt;span class="s1"&gt;'first_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'600'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// 10 min for new/guest sessions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For B2B Magento stores with account-heavy workflows, longer lifetimes (90 days) reduce friction. For high-traffic B2C stores, shorter guest session lifetimes aggressively keep Redis memory usage down.&lt;/p&gt;

&lt;p&gt;Run this command to check your current session count and memory usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli &lt;span class="nt"&gt;-n&lt;/span&gt; 2 info keyspace
redis-cli &lt;span class="nt"&gt;-n&lt;/span&gt; 2 dbsize
redis-cli info memory | &lt;span class="nb"&gt;grep &lt;/span&gt;used_memory_human
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Flushing Sessions Safely
&lt;/h2&gt;

&lt;p&gt;Never run &lt;code&gt;redis-cli flushall&lt;/code&gt; in production — it wipes all Redis data including cache. To flush only sessions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli &lt;span class="nt"&gt;-n&lt;/span&gt; 2 flushdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To expire sessions for a specific customer (useful for security incidents), find and delete their session key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli &lt;span class="nt"&gt;-n&lt;/span&gt; 2 keys &lt;span class="s2"&gt;"sess_*"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
redis-cli &lt;span class="nt"&gt;-n&lt;/span&gt; 2 del sess_&amp;lt;session_id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verifying Your Configuration
&lt;/h2&gt;

&lt;p&gt;After updating &lt;code&gt;env.php&lt;/code&gt;, flush config and verify Redis is receiving session writes:&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 cache:flush config
redis-cli &lt;span class="nt"&gt;-n&lt;/span&gt; 2 monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open your store in a browser and watch &lt;code&gt;redis-cli monitor&lt;/code&gt; — you should see &lt;code&gt;SET sess_*&lt;/code&gt; and &lt;code&gt;GET sess_*&lt;/code&gt; commands flowing in.&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;&lt;/th&gt;
&lt;th&gt;Filesystem&lt;/th&gt;
&lt;th&gt;MySQL DB&lt;/th&gt;
&lt;th&gt;Redis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Slow&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-server&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-expiry&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory safe&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Needs tuning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recommended&lt;/td&gt;
&lt;td&gt;Dev only&lt;/td&gt;
&lt;td&gt;Fallback&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Production&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're running Magento 2 in production and haven't switched to Redis sessions yet, it's the single highest-impact configuration change you can make with the least risk. It's well-supported, well-documented, and the performance difference under real traffic is immediately measurable.&lt;/p&gt;

&lt;p&gt;Start with a separate Redis database number, configure sane TTLs for bots and guests, and make sure &lt;code&gt;maxmemory-policy&lt;/code&gt; is set to &lt;code&gt;noeviction&lt;/code&gt; for the session database. Your checkout conversion rate (and your ops team) will thank you.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>redis</category>
      <category>performance</category>
      <category>php</category>
    </item>
    <item>
      <title>Magento 2 Async Operations and Message Queues: A Performance Deep Dive</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Fri, 08 May 2026 07:42:57 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-async-operations-and-message-queues-a-performance-deep-dive-42no</link>
      <guid>https://dev.to/magevanta/magento-2-async-operations-and-message-queues-a-performance-deep-dive-42no</guid>
      <description>&lt;p&gt;Every Magento 2 store has operations that are too slow to run synchronously during a web request. Sending transactional emails. Updating inventory across thousands of products. Re-indexing after bulk catalog changes. Running price calculations for complex customer segments.&lt;/p&gt;

&lt;p&gt;The naive approach — doing it all in-process during the HTTP request — results in timeouts, frustrated customers, and overwhelmed PHP workers. Magento 2's message queue framework is the proper solution, and when configured correctly, it can dramatically improve perceived performance while making your architecture more resilient.&lt;/p&gt;

&lt;p&gt;This guide covers everything you need to know about Magento 2's async operations and message queue system: the architecture, RabbitMQ setup, consumer tuning, and practical patterns for pushing your own heavy workloads off the critical path.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is the Message Queue Framework?
&lt;/h2&gt;

&lt;p&gt;Magento 2's message queue framework (introduced in 2.0 for B2B, available in all editions since 2.2) is a pub/sub system that decouples producers (code that initiates work) from consumers (code that processes it).&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request → Process → Wait → Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request → Publish message → Return immediately
          (Consumer picks up message and processes asynchronously)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The framework supports two backends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MySQL&lt;/strong&gt; — Simple, no extra infrastructure, works out of the box. Limited throughput.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RabbitMQ&lt;/strong&gt; — Enterprise-grade, high-throughput, supports complex routing. Recommended for production.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Magento Already Uses Message Queues For
&lt;/h2&gt;

&lt;p&gt;Before building custom async logic, it's worth knowing what Magento already processes asynchronously:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;async.operations.all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bulk REST API operations (product updates, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inventory.reservations.updateSalabilityStatus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MSI stock updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sales.rule.quote.trigger.recollect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cart price rule recalculations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;product_action_attribute.update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bulk attribute updates from Admin grid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;async.magento.cataloginventory.api.stockregistryinterface.updatestockitembysku.post&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Async stock updates via API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're using bulk operations in Admin or the Async REST API, these queues are already doing work for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The message queue system has four key components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Publisher&lt;/strong&gt; — Sends a message to a topic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topic&lt;/strong&gt; — Named channel (e.g., &lt;code&gt;mycompany.product.sync&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue&lt;/strong&gt; — Storage for pending messages (MySQL table or RabbitMQ queue)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer&lt;/strong&gt; — Long-running process that polls the queue and processes messages&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Configuration is split across three XML files in your module:&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="s"&gt;etc/&lt;/span&gt;
&lt;span class="s"&gt;├── queue_topology.xml&lt;/span&gt;   &lt;span class="c1"&gt;# Defines exchanges and bindings&lt;/span&gt;
&lt;span class="s"&gt;├── queue_publisher.xml&lt;/span&gt;  &lt;span class="c1"&gt;# Connects topics to exchanges&lt;/span&gt;
&lt;span class="s"&gt;├── queue_consumer.xml&lt;/span&gt;   &lt;span class="c1"&gt;# Defines consumer handlers&lt;/span&gt;
&lt;span class="s"&gt;└── communication.xml&lt;/span&gt;    &lt;span class="c1"&gt;# Defines topic schemas&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting Up RabbitMQ
&lt;/h2&gt;

&lt;p&gt;For anything beyond development, use RabbitMQ. MySQL queues work but have limitations under load — they use polling, table locks, and don't support priority queues or dead letter exchanges.&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;# Ubuntu/Debian&lt;/span&gt;
apt &lt;span class="nb"&gt;install &lt;/span&gt;rabbitmq-server
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;rabbitmq-server
systemctl start rabbitmq-server

&lt;span class="c"&gt;# Enable management UI&lt;/span&gt;
rabbitmq-plugins &lt;span class="nb"&gt;enable &lt;/span&gt;rabbitmq_management
&lt;span class="c"&gt;# Access at http://localhost:15672 (guest/guest)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Magento Configuration
&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;// app/etc/env.php&lt;/span&gt;
&lt;span class="s1"&gt;'queue'&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;'amqp'&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;'host'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'5672'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'user'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'magento'&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="s1"&gt;'your-password'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'virtualhost'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'ssl'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&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;Create a dedicated RabbitMQ user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rabbitmqctl add_user magento your-password
rabbitmqctl set_permissions &lt;span class="nt"&gt;-p&lt;/span&gt; / magento &lt;span class="s2"&gt;".*"&lt;/span&gt; &lt;span class="s2"&gt;".*"&lt;/span&gt; &lt;span class="s2"&gt;".*"&lt;/span&gt;
rabbitmqctl set_user_tags magento monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verify the Connection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento queue:consumers:list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this returns a list of consumers without errors, the AMQP connection is working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Custom Async Operation
&lt;/h2&gt;

&lt;p&gt;Let's walk through a real example: a custom product sync to an external ERP system that currently runs synchronously during the admin product save.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define the Topic Schema
&lt;/h3&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;!-- etc/communication.xml --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?xml version="1.0"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;config&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
        &lt;span class="na"&gt;xsi:noNamespaceSchemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"urn:magento:framework:Communication/etc/communication.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;topic&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"mycompany.erp.product.sync"&lt;/span&gt; &lt;span class="na"&gt;request=&lt;/span&gt;&lt;span class="s"&gt;"MyCompany\ErpSync\Api\Data\SyncRequestInterface"&lt;/span&gt;&lt;span class="nt"&gt;/&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;h3&gt;
  
  
  Step 2: Configure the Publisher
&lt;/h3&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;!-- etc/queue_publisher.xml --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?xml version="1.0"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;config&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
        &lt;span class="na"&gt;xsi:noNamespaceSchemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"urn:magento:framework-message-queue:etc/publisher.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;publisher&lt;/span&gt; &lt;span class="na"&gt;topic=&lt;/span&gt;&lt;span class="s"&gt;"mycompany.erp.product.sync"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;connection&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"amqp"&lt;/span&gt; &lt;span class="na"&gt;exchange=&lt;/span&gt;&lt;span class="s"&gt;"magento"&lt;/span&gt; &lt;span class="na"&gt;disabled=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Fallback to MySQL if RabbitMQ unavailable --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;connection&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"db"&lt;/span&gt; &lt;span class="na"&gt;exchange=&lt;/span&gt;&lt;span class="s"&gt;"magento-db"&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;/publisher&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;h3&gt;
  
  
  Step 3: Configure Topology (RabbitMQ)
&lt;/h3&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;!-- etc/queue_topology.xml --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?xml version="1.0"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;config&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
        &lt;span class="na"&gt;xsi:noNamespaceSchemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"urn:magento:framework-message-queue:etc/topology.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;exchange&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"magento"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"topic"&lt;/span&gt; &lt;span class="na"&gt;connection=&lt;/span&gt;&lt;span class="s"&gt;"amqp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;binding&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"erpSyncBinding"&lt;/span&gt;
                 &lt;span class="na"&gt;topic=&lt;/span&gt;&lt;span class="s"&gt;"mycompany.erp.product.sync"&lt;/span&gt;
                 &lt;span class="na"&gt;destinationType=&lt;/span&gt;&lt;span class="s"&gt;"queue"&lt;/span&gt;
                 &lt;span class="na"&gt;destination=&lt;/span&gt;&lt;span class="s"&gt;"mycompany.erp.product.sync"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/exchange&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;h3&gt;
  
  
  Step 4: Define the Consumer
&lt;/h3&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;!-- etc/queue_consumer.xml --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?xml version="1.0"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;config&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
        &lt;span class="na"&gt;xsi:noNamespaceSchemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"urn:magento:framework-message-queue:etc/consumer.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;consumer&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"myCompanyErpProductSync"&lt;/span&gt;
              &lt;span class="na"&gt;queue=&lt;/span&gt;&lt;span class="s"&gt;"mycompany.erp.product.sync"&lt;/span&gt;
              &lt;span class="na"&gt;connection=&lt;/span&gt;&lt;span class="s"&gt;"amqp"&lt;/span&gt;
              &lt;span class="na"&gt;consumerInstance=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Framework\MessageQueue\Consumer"&lt;/span&gt;
              &lt;span class="na"&gt;handler=&lt;/span&gt;&lt;span class="s"&gt;"MyCompany\ErpSync\Model\ProductSyncHandler::processMessage"&lt;/span&gt;
              &lt;span class="na"&gt;maxMessages=&lt;/span&gt;&lt;span class="s"&gt;"1000"&lt;/span&gt;&lt;span class="nt"&gt;/&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;h3&gt;
  
  
  Step 5: Implement the Handler
&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;// Model/ProductSyncHandler.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyCompany\ErpSync\Model&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;MyCompany\ErpSync\Api\Data\SyncRequestInterface&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;Psr\Log\LoggerInterface&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;ProductSyncHandler&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="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;ErpApiClient&lt;/span&gt; &lt;span class="nv"&gt;$erpClient&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;readonly&lt;/span&gt; &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$logger&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;processMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;SyncRequestInterface&lt;/span&gt; &lt;span class="nv"&gt;$request&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="k"&gt;try&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;erpClient&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;syncProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSku&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getProductData&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;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&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 but don't re-throw — Magento will handle dead letters&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ERP sync failed: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'sku'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSku&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;h3&gt;
  
  
  Step 6: Publish from Your Event Observer
&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;// Observer/ProductSaveAfter.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyCompany\ErpSync\Observer&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\MessageQueue\PublisherInterface&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;MyCompany\ErpSync\Api\Data\SyncRequestInterfaceFactory&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;ProductSaveAfter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;\Magento\Framework\Event\ObserverInterface&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="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;PublisherInterface&lt;/span&gt; &lt;span class="nv"&gt;$publisher&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;readonly&lt;/span&gt; &lt;span class="kt"&gt;SyncRequestInterfaceFactory&lt;/span&gt; &lt;span class="nv"&gt;$requestFactory&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="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;$product&lt;/span&gt; &lt;span class="o"&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;getEvent&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;getProduct&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$request&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;requestFactory&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="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setSku&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSku&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setProductData&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;extractProductData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Non-blocking: returns immediately, consumer picks up async&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;'mycompany.erp.product.sync'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$request&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 admin product save now returns instantly. The ERP sync happens in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Consumers in Production
&lt;/h2&gt;

&lt;p&gt;Consumer processes are long-running PHP daemons. They need to be managed like a service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supervisor Configuration
&lt;/h3&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-consumers.conf
&lt;/span&gt;&lt;span class="nn"&gt;[program:magento-erp-sync]&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;/usr/bin/php /var/www/html/bin/magento queue:consumers:start myCompanyErpProductSync --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;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;www-data&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;startretries&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&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/supervisor/magento-erp-sync.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/supervisor/magento-erp-sync.out.log&lt;/span&gt;
&lt;span class="py"&gt;stopasgroup&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;killasgroup&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[program:magento-inventory-reservations]&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;/usr/bin/php /var/www/html/bin/magento queue:consumers:start inventoryQtyCounter --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;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;www-data&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload Supervisor after changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;supervisorctl reread
supervisorctl update
supervisorctl status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Consumer Options
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento queue:consumers:start &amp;lt;consumer-name&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10000 &lt;span class="se"&gt;\ &lt;/span&gt;  &lt;span class="c"&gt;# Restart after N messages (prevents memory leaks)&lt;/span&gt;
  &lt;span class="nt"&gt;--batch-size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100 &lt;span class="se"&gt;\ &lt;/span&gt;      &lt;span class="c"&gt;# Process N messages per iteration&lt;/span&gt;
  &lt;span class="nt"&gt;--single-thread&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt;       &lt;span class="c"&gt;# Run as single-threaded (default)&lt;/span&gt;
  &lt;span class="nt"&gt;--pid-file-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/consumer.pid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--max-messages&lt;/code&gt; flag is critical. Long-running PHP processes accumulate memory leaks from Magento's object manager. Setting a reasonable limit (1000-50000 depending on message size) and letting Supervisor restart the process is safer than running indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling Consumers
&lt;/h2&gt;

&lt;p&gt;For high-throughput scenarios, run multiple consumer instances in parallel:&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;; Multiple parallel consumers for high-volume queue
&lt;/span&gt;&lt;span class="nn"&gt;[program:magento-erp-sync]&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;/usr/bin/php /var/www/html/bin/magento queue:consumers:start myCompanyErpProductSync --max-messages=5000&lt;/span&gt;
&lt;span class="py"&gt;numprocs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;4               ; Run 4 parallel consumers&lt;/span&gt;
&lt;span class="py"&gt;process_name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%(program_name)s-%(process_num)02d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With RabbitMQ, multiple consumers on the same queue use round-robin distribution automatically. With MySQL queues, Magento handles locking to prevent double-processing, but MySQL queues don't scale as gracefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Async Bulk Operations API
&lt;/h2&gt;

&lt;p&gt;For mass operations (importing thousands of products, bulk price updates, etc.), Magento's Async Bulk REST API is built on top of the message queue framework:&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;# Async bulk product update&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://yourstore.com/rest/async/bulk/V1/products &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&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;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'[
    {"product": {"sku": "SKU001", "price": 29.99}},
    {"product": {"sku": "SKU002", "price": 39.99}},
    ...
  ]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response returns immediately with a bulk operation UUID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bulk_uuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3d4-..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check status asynchronously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://yourstore.com/rest/V1/bulk/a1b2c3d4-.../status &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach is dramatically faster for bulk imports than the synchronous REST API because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The HTTP request returns in milliseconds&lt;/li&gt;
&lt;li&gt;Operations are batched and processed by consumers&lt;/li&gt;
&lt;li&gt;Multiple consumers can parallelize the work&lt;/li&gt;
&lt;li&gt;Failed operations are tracked and retriable without re-running everything&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Monitoring Queue Health
&lt;/h2&gt;

&lt;h3&gt;
  
  
  RabbitMQ Management Console
&lt;/h3&gt;

&lt;p&gt;The RabbitMQ management UI (&lt;code&gt;http://your-server:15672&lt;/code&gt;) gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queue depths (are messages piling up?)&lt;/li&gt;
&lt;li&gt;Consumer counts (are consumers running?)&lt;/li&gt;
&lt;li&gt;Message rates (throughput per second)&lt;/li&gt;
&lt;li&gt;Dead letter queue contents (failed messages)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CLI Monitoring
&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;# List all queues and their message counts&lt;/span&gt;
rabbitmqctl list_queues name messages consumers

&lt;span class="c"&gt;# Check if consumers are running&lt;/span&gt;
bin/magento queue:consumers:list

&lt;span class="c"&gt;# Magento's bulk operation status&lt;/span&gt;
bin/magento queue:consumers:start &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Alert on Queue Depth
&lt;/h3&gt;

&lt;p&gt;Set up monitoring to alert when queue depth exceeds thresholds:&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;# Simple bash check (add to cron or monitoring system)&lt;/span&gt;
&lt;span class="nv"&gt;DEPTH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;rabbitmqctl list_queues name messages | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"mycompany.erp.product.sync"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEPTH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 1000 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"WARNING: ERP sync queue depth is &lt;/span&gt;&lt;span class="nv"&gt;$DEPTH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Queue Alert"&lt;/span&gt; ops@yourcompany.com
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Pitfalls and How to Avoid Them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pitfall 1: Not Handling Failures
&lt;/h3&gt;

&lt;p&gt;If your consumer throws an unhandled exception, the message may be lost (MySQL) or moved to a dead letter queue (RabbitMQ). Always catch exceptions in your handler and implement retry logic or alerting for persistent failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 2: Running Consumers via Cron Only
&lt;/h3&gt;

&lt;p&gt;Magento's default setup uses cron to start consumers. This adds up to a 1-minute delay before a consumer starts processing new messages. For latency-sensitive operations, run consumers as persistent daemons via Supervisor instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 3: Large Message Payloads
&lt;/h3&gt;

&lt;p&gt;Message queues are optimized for small messages. Avoid putting large data blobs (full product objects, images) in messages. Instead, pass identifiers:&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;// Bad: serialize the entire product&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;'topic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// Could be 50KB+&lt;/span&gt;

&lt;span class="c1"&gt;// Good: pass just the ID&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;'topic'&lt;/span&gt;&lt;span class="p"&gt;,&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;$product&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="c1"&gt;// Bytes&lt;/span&gt;
&lt;span class="c1"&gt;// Consumer loads the product from DB when processing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pitfall 4: Ignoring Memory Limits
&lt;/h3&gt;

&lt;p&gt;Long-running consumers will eventually exhaust PHP memory without &lt;code&gt;--max-messages&lt;/code&gt;. The Supervisor restart approach is the standard pattern — don't fight it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Reference: When to Use Message Queues
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Async?&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sending transactional emails&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;No customer-facing impact from delay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Syncing to external ERP/PIM&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;External API latency shouldn't block save&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulk product/price updates&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Can take minutes; use Async Bulk API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Re-indexing after bulk import&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Resource-intensive, not time-critical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updating cart totals&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;Customer expects immediate feedback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inventory check at checkout&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;Must be synchronous for accuracy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generating invoices&lt;/td&gt;
&lt;td&gt;⚠️ Depends&lt;/td&gt;
&lt;td&gt;Async OK if email delivery is the only output&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;Magento 2's message queue framework is one of the platform's most underutilized performance tools. Most stores that struggle with slow admin saves, timeout-prone bulk operations, or overloaded PHP workers could benefit significantly from pushing work off the critical path.&lt;/p&gt;

&lt;p&gt;The investment is real — you need RabbitMQ in production, Supervisor to manage consumers, and careful attention to message schema design. But the payoff is equally real: admin product saves that return in milliseconds, bulk imports that don't time out, and an architecture that handles traffic spikes gracefully because expensive work is queued rather than executed inline.&lt;/p&gt;

&lt;p&gt;Start with Magento's built-in async operations (the Async Bulk REST API is available today with no custom code), then identify the top 2-3 synchronous operations in your codebase that are causing the most pain, and implement them as async consumers. Your customers — and your PHP workers — will thank you.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Magento 2 Layout XML Performance: Trim Blocks, Speed Up Rendering</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Fri, 08 May 2026 07:42:49 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-layout-xml-performance-trim-blocks-speed-up-rendering-1bm8</link>
      <guid>https://dev.to/magevanta/magento-2-layout-xml-performance-trim-blocks-speed-up-rendering-1bm8</guid>
      <description>&lt;p&gt;Magento 2's layout XML system is the backbone of every page render. It controls which blocks get instantiated, which templates get rendered, and how the page tree is assembled before PHP hands off output to the browser. It's also one of the most overlooked performance vectors in the platform.&lt;/p&gt;

&lt;p&gt;Unlike database queries or JavaScript bundles, layout XML overhead is invisible in standard monitoring. You won't see it in a slow query log. It won't show up as a large network request. It hides inside TTFB — Time to First Byte — silently burning CPU cycles while your server assembles the block tree.&lt;/p&gt;

&lt;p&gt;This guide dives deep into Magento 2 layout XML performance: how to audit what's being rendered, how to remove what isn't needed, and how to use caching and lazy rendering to get the most out of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Magento 2 Layout XML Works
&lt;/h2&gt;

&lt;p&gt;Before optimizing, understand the rendering pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Layout handle collection&lt;/strong&gt; — Magento collects all applicable layout handles for the current page (e.g., &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;cms_index_index&lt;/code&gt;, &lt;code&gt;catalog_category_view_type_layered&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XML merging&lt;/strong&gt; — All XML files matching those handles are merged into a single layout tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block instantiation&lt;/strong&gt; — Every &lt;code&gt;&amp;lt;block&amp;gt;&lt;/code&gt; element in the tree is instantiated as a PHP object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template rendering&lt;/strong&gt; — Each block renders its &lt;code&gt;.phtml&lt;/code&gt; template, recursively rendering child blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output assembly&lt;/strong&gt; — The rendered HTML is assembled and sent to the client (or written to FPC).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The problem: Magento instantiates &lt;strong&gt;all blocks in the tree&lt;/strong&gt;, even ones whose output is never used on the current page. Third-party extensions are notorious for adding blocks to &lt;code&gt;default.xml&lt;/code&gt; that run expensive logic on every single page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auditing Your Layout Tree
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Enable Layout Debug Output
&lt;/h3&gt;

&lt;p&gt;The fastest way to see what's being rendered is to enable Magento's built-in template hints:&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 dev:template-hints:enable &lt;span class="nt"&gt;--type&lt;/span&gt; frontend
bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to your store with &lt;code&gt;?templatehints=magento&lt;/code&gt; appended to the URL (you may need to set a hint secret in config). Every block will be outlined with its class name and template path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use the Profiler
&lt;/h3&gt;

&lt;p&gt;Enable Magento's built-in profiler for deeper insight:&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 dev:profiler:enable html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit a page and look at the profiler output at the bottom. Sort by &lt;code&gt;sum&lt;/code&gt; to find the slowest blocks. Look for anything over 50ms that you don't recognize.&lt;/p&gt;

&lt;p&gt;Disable the profiler when done:&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 dev:profiler:disable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Programmatic Block Tree Inspection
&lt;/h3&gt;

&lt;p&gt;For a non-visual audit, you can dump the full layout structure from 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;# See all layout handles for a given page type&lt;/span&gt;
bin/magento dev:layout:deps &lt;span class="nt"&gt;--theme&lt;/span&gt; Magento/luma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or write a quick script to dump the merged layout XML for a specific handle:&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;$layout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$objectManager&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="nc"&gt;\Magento\Framework\View\LayoutInterface&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$layout&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getUpdate&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;load&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'default'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'catalog_product_view'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$layout&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getUpdate&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;asString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Top Performance Offenders
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Blocks in &lt;code&gt;default.xml&lt;/code&gt; with Heavy Constructors
&lt;/h3&gt;

&lt;p&gt;Extensions often add blocks to &lt;code&gt;default.xml&lt;/code&gt; because it's the safest choice for the developer — the block will always be there. But this means the block is instantiated on every single page, including your homepage, product pages, and checkout.&lt;/p&gt;

&lt;p&gt;Look for patterns like this in &lt;code&gt;vendor/&lt;/code&gt; or &lt;code&gt;app/code/&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/somevendor/somemodule/view/frontend/layout/default.xml --&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;"after.body.start"&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;"SomeVendor\SomeModule\Block\Tracker"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"somevendor.tracker"&lt;/span&gt; &lt;span class="na"&gt;template=&lt;/span&gt;&lt;span class="s"&gt;"tracker.phtml"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the block constructor makes a database call, hits an external API, or loads configuration, it runs on &lt;strong&gt;every page&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use a &lt;code&gt;\Magento\Framework\View\Element\Template&lt;/code&gt; subclass and defer logic to &lt;code&gt;_toHtml()&lt;/code&gt; or &lt;code&gt;getCacheLifetime()&lt;/code&gt;, or move the block to a specific layout handle if it's only needed on certain pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Excessive Layout Handles
&lt;/h3&gt;

&lt;p&gt;Every additional layout handle merges more XML, which means more parsing and more blocks. Some stores accumulate dozens of custom layout handles from extensions that each add their own block modifications.&lt;/p&gt;

&lt;p&gt;Check how many handles are loaded on a typical page:&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;$handles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$layout&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getUpdate&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;getHandles&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$handles&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// Ideally &amp;lt; 20 on a typical page&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're seeing 40+ handles, investigate which extensions are adding them and whether they're all necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Widget Instances
&lt;/h3&gt;

&lt;p&gt;Magento widgets are powerful but expensive. Each widget instance triggers a database query to load its configuration and an additional layout merge. Stores with dozens of active widgets accumulate significant per-request overhead.&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;-- Check your widget count&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="n"&gt;instance_type&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;widget_instance&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;instance_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="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;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have 50+ widget instances, consider replacing frequently-used ones with hardcoded blocks or static HTML. Widgets that appear on every page (via &lt;code&gt;all pages&lt;/code&gt; page group setting) are especially expensive.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Unnecessary &lt;code&gt;&amp;lt;remove&amp;gt;&lt;/code&gt; Calls
&lt;/h3&gt;

&lt;p&gt;Many themes and extensions use &lt;code&gt;&amp;lt;remove name="block.name"/&amp;gt;&lt;/code&gt; to hide blocks. This is wasteful — the block is still instantiated before being removed. The correct approach is:&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;!-- Wrong: instantiates block then removes it --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;remove&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"breadcrumbs"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Better: prevent rendering via remove attribute --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;referenceBlock&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"breadcrumbs"&lt;/span&gt; &lt;span class="na"&gt;remove=&lt;/span&gt;&lt;span class="s"&gt;"true"&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;The &lt;code&gt;remove="true"&lt;/code&gt; attribute on &lt;code&gt;&amp;lt;referenceBlock&amp;gt;&lt;/code&gt; prevents rendering without instantiating unnecessarily. The cleanest approach is to override the parent layout and simply not include the block in the first place.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. &lt;code&gt;_prepareLayout()&lt;/code&gt; Anti-Patterns
&lt;/h3&gt;

&lt;p&gt;Blocks that execute expensive logic in &lt;code&gt;_prepareLayout()&lt;/code&gt; run during layout building, before any output is generated. This is particularly bad because &lt;code&gt;_prepareLayout()&lt;/code&gt; runs even if the block's output is cached.&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;// Bad: runs on every page even if block output is cached&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;_prepareLayout&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;productCollection&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;collectionFactory&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addAttributeToSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'*'&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;setPageSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&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;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Full collection load in _prepareLayout!&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;_prepareLayout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Better: lazy load in getter&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;getProducts&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;productCollection&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;productCollection&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;collectionFactory&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addAttributeToSelect&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'price'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'url_key'&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;setPageSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&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;load&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;productCollection&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;
  
  
  Block Caching: Your Most Powerful Tool
&lt;/h2&gt;

&lt;p&gt;Magento's block cache is often the highest-leverage optimization available. If a block's output doesn't change between requests (or changes only per customer group, store, etc.), cache it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Cache to a Block
&lt;/h3&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;\Magento\Framework\View\Element\Template&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;getCacheLifetime&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="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="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;getCacheKeyInfo&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="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'MY_BLOCK'&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;_storeManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStore&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;_design&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDesignTheme&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;httpContext&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="nc"&gt;\Magento\Customer\Model\Context&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CONTEXT_GROUP&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;The &lt;code&gt;getCacheKeyInfo()&lt;/code&gt; method determines cache uniqueness. Include everything that affects the block's output — store ID, theme, customer group — but nothing more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Varnish + ESI for Partial Caching
&lt;/h3&gt;

&lt;p&gt;For blocks that need to be personalized per customer but are otherwise static, consider Edge Side Includes (ESI) with Varnish. The main page is fully cached; the personalized block is fetched separately via a fast ESI request:&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;!-- In your block's template --&amp;gt;&lt;/span&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;"&amp;lt;?= $block-&amp;gt;getEsiUrl() ?&amp;gt;"&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;This is especially effective for the minicart, customer welcome message, and recently viewed products — blocks that prevent FPC from caching an otherwise static page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing Block Count in Critical Pages
&lt;/h2&gt;

&lt;p&gt;On high-traffic pages (homepage, category pages, product pages), aggressively audit the block tree and remove anything unnecessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remove Default Magento Blocks You Don't Use
&lt;/h3&gt;

&lt;p&gt;Magento's default layout includes many blocks that most stores don't need:&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;!-- In your theme's default.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;page&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Remove if you don't use compare functionality --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;referenceBlock&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"catalog.compare.sidebar"&lt;/span&gt; &lt;span class="na"&gt;remove=&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="c"&gt;&amp;lt;!-- Remove if you use a custom cookie notice --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;referenceBlock&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"cookie-status-message"&lt;/span&gt; &lt;span class="na"&gt;remove=&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="c"&gt;&amp;lt;!-- Remove newsletter block if handled differently --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;referenceBlock&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"form.subscribe"&lt;/span&gt; &lt;span class="na"&gt;remove=&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;/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;h3&gt;
  
  
  Defer Below-the-Fold Blocks
&lt;/h3&gt;

&lt;p&gt;Blocks that appear below the fold (product tabs, related products, upsells) don't need to render synchronously. Consider using lazy-loading via JavaScript:&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;!-- Replace block with a container that JS will populate --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;referenceBlock&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"product.info.upsell"&lt;/span&gt; &lt;span class="na"&gt;remove=&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="c"&gt;&amp;lt;!-- Add a lightweight placeholder instead --&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;"product.info.main"&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;"Magento\Framework\View\Element\Template"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"upsell.lazy.placeholder"&lt;/span&gt;
           &lt;span class="na"&gt;template=&lt;/span&gt;&lt;span class="s"&gt;"Your_Module::upsell-lazy.phtml"&lt;/span&gt; &lt;span class="na"&gt;after=&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;/referenceContainer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then load the actual upsell content via AJAX after the main page has rendered. This moves the expensive collection load off the critical path entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layout XML Merge Caching
&lt;/h2&gt;

&lt;p&gt;Magento caches the merged layout XML in the cache backend. If this cache is being invalidated too frequently, you'll pay the merge cost repeatedly.&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 layout cache keys (Redis backend)&lt;/span&gt;
redis-cli &lt;span class="nt"&gt;-n&lt;/span&gt; 0 keys &lt;span class="s2"&gt;"LAYOUT_*"&lt;/span&gt; | &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;Causes of frequent layout cache invalidation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploying static content without flushing properly&lt;/li&gt;
&lt;li&gt;Extensions that call &lt;code&gt;$layout-&amp;gt;getUpdate()-&amp;gt;addUpdate()&lt;/code&gt; with dynamic content&lt;/li&gt;
&lt;li&gt;Incorrect &lt;code&gt;cache_tag&lt;/code&gt; configuration in blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ensure your deployment process flushes layout cache cleanly:&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 cache:flush layout block_html full_page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Practical Checklist
&lt;/h2&gt;

&lt;p&gt;Work through this list systematically on any store showing slow TTFB:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Profile with Magento Profiler — identify blocks taking &amp;gt;20ms&lt;/li&gt;
&lt;li&gt;[ ] Audit &lt;code&gt;default.xml&lt;/code&gt; across all modules for blocks with heavy constructors&lt;/li&gt;
&lt;li&gt;[ ] Count layout handles on key page types — target under 20&lt;/li&gt;
&lt;li&gt;[ ] Review all blocks missing &lt;code&gt;getCacheLifetime()&lt;/code&gt; — add caching where possible&lt;/li&gt;
&lt;li&gt;[ ] Check widget count — replace high-frequency widgets with static blocks&lt;/li&gt;
&lt;li&gt;[ ] Audit &lt;code&gt;_prepareLayout()&lt;/code&gt; in custom and extension blocks — move logic to lazy getters&lt;/li&gt;
&lt;li&gt;[ ] Remove unused default Magento blocks from your theme&lt;/li&gt;
&lt;li&gt;[ ] Verify layout cache is warming after deploys&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Use these commands before and after optimization to quantify improvements:&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;# TTFB measurement via curl (bypasses FPC with cache-busting header)&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;"TTFB: %{time_starttransfer}s&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="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Cache-Control: no-cache"&lt;/span&gt; https://yourstore.com/your-category

&lt;span class="c"&gt;# Run 5 times and average&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="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_starttransfer}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://yourstore.com/your-category
&lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{sum+=$1} END {print "Average TTFB:", sum/NR, "seconds"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a well-optimized Magento 2 store on capable hardware, layout instantiation time should be under 50ms on category pages and under 30ms on CMS pages. If you're seeing 200ms+, there's almost certainly a block with an expensive constructor loading on the &lt;code&gt;default&lt;/code&gt; handle.&lt;/p&gt;

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

&lt;p&gt;Layout XML performance is invisible until it isn't. Stores that have accumulated years of extensions — each adding a few blocks to &lt;code&gt;default.xml&lt;/code&gt; — can find themselves spending 500ms+ just instantiating blocks before a single line of HTML is rendered.&lt;/p&gt;

&lt;p&gt;The good news is that layout optimizations are highly leveraged: fixing one slow block in &lt;code&gt;default.xml&lt;/code&gt; improves every single page on your store. Start with the profiler, identify your top offenders, and work through the checklist above. Combined with proper block caching and FPC, a well-tuned layout XML configuration can shave hundreds of milliseconds off your TTFB without touching a single line of business logic.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Magento 2 Checkout Performance Optimization: Reduce Drop-offs and Speed Up Conversions</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Thu, 07 May 2026 09:03:52 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-checkout-performance-optimization-reduce-drop-offs-and-speed-up-conversions-26e8</link>
      <guid>https://dev.to/magevanta/magento-2-checkout-performance-optimization-reduce-drop-offs-and-speed-up-conversions-26e8</guid>
      <description>&lt;p&gt;Checkout is the most critical page in any Magento 2 store. A one-second delay at this stage can cost you significantly more than the same delay on a product page — studies consistently show that conversion rates drop 7% for every second of checkout load time. Yet the Magento 2 checkout is notoriously heavy: it loads dozens of JavaScript components, makes multiple AJAX calls, and queries several database tables on every step.&lt;/p&gt;

&lt;p&gt;This guide walks through the most impactful optimizations you can make to speed up your checkout — from quick wins to deep infrastructure changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Is Magento 2 Checkout So Slow?
&lt;/h2&gt;

&lt;p&gt;Before optimizing, it helps to understand the bottlenecks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;KnockoutJS + RequireJS overhead&lt;/strong&gt; — The checkout is built on KnockoutJS with dozens of UI components, each requiring its own module load via RequireJS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple AJAX requests on page load&lt;/strong&gt; — Shipping methods, totals, customer data, and cart summaries are all fetched asynchronously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Page Cache exclusion&lt;/strong&gt; — The checkout is not cacheable by FPC, so every request hits the application stack directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party payment/shipping extensions&lt;/strong&gt; — These often add their own JS, CSS, and AJAX calls, compounding the overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quote recalculation&lt;/strong&gt; — Every time a customer changes address, coupon, or shipping method, Magento recalculates the entire quote.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Enable and Tune Full Page Cache (Even for Checkout)
&lt;/h2&gt;

&lt;p&gt;While the checkout page itself cannot be fully cached, there are adjacent pages that can dramatically affect perceived checkout speed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;cart page&lt;/strong&gt; should load instantly. Ensure it is FPC-cached.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;minicart&lt;/strong&gt; data can be warmed and stored in browser &lt;code&gt;localStorage&lt;/code&gt; to prevent repeated AJAX calls.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Varnish + ESI&lt;/strong&gt; (Edge Side Includes) to cache static parts of checkout-adjacent pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More importantly, ensure FPC is enabled and warm for the rest of your store, because a fast product → cart → checkout funnel depends on each step being optimized.&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;# Verify FPC is enabled&lt;/span&gt;
bin/magento cache:status | &lt;span class="nb"&gt;grep &lt;/span&gt;full_page

&lt;span class="c"&gt;# Flush and warm FPC&lt;/span&gt;
bin/magento cache:flush full_page
bin/magento cache:warm:fpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. JavaScript Optimization for Checkout
&lt;/h2&gt;

&lt;p&gt;The checkout page is JavaScript-heavy by nature. The goal is to reduce parse time and eliminate blocking resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merge and Bundle JS
&lt;/h3&gt;

&lt;p&gt;Enable JS merging in &lt;strong&gt;Admin &amp;gt; Stores &amp;gt; Configuration &amp;gt; Advanced &amp;gt; Developer &amp;gt; JavaScript Settings&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Merge JavaScript Files:&lt;/strong&gt; Yes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable JavaScript Bundling:&lt;/strong&gt; Yes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minify JavaScript Files:&lt;/strong&gt; Yes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For production, use the built-in bundling or switch to a more advanced bundler:&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;--jobs&lt;/span&gt; 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Use Advanced JS Bundling
&lt;/h3&gt;

&lt;p&gt;Magento's default bundling is often suboptimal. Consider using the &lt;a href="https://developer.adobe.com/commerce/frontend-core/guide/themes/js-bundling/" rel="noopener noreferrer"&gt;Magento Advanced Bundling&lt;/a&gt; approach with Grunt, which creates page-type-specific bundles so the checkout only loads what it needs.&lt;/p&gt;

&lt;p&gt;Alternatively, community tools like &lt;strong&gt;Mirasvit JS Bundle&lt;/strong&gt; or &lt;strong&gt;Yireo JS Bundler&lt;/strong&gt; provide more sophisticated dependency graph analysis and can reduce initial checkout JS payload by 40–60%.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defer Non-Critical Scripts
&lt;/h3&gt;

&lt;p&gt;Move non-critical third-party scripts (analytics, chat widgets, social pixels) to load after the checkout has initialized:&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/checkout_index_index.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;page&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;block&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"your.analytics"&lt;/span&gt; &lt;span class="na"&gt;template=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="na"&gt;after=&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="c"&gt;&amp;lt;!-- Load after checkout init --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/block&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;h2&gt;
  
  
  3. Reduce AJAX Calls During Checkout
&lt;/h2&gt;

&lt;p&gt;Every AJAX call in checkout adds latency. Here's how to minimize them:&lt;/p&gt;

&lt;h3&gt;
  
  
  Customer Data Sections
&lt;/h3&gt;

&lt;p&gt;Magento's "sections" system (customer data, cart, etc.) makes AJAX calls to &lt;code&gt;/customer/section/load/&lt;/code&gt; on various triggers. Audit which sections are being reloaded unnecessarily:&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 which sections are defined&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;"sections"&lt;/span&gt; vendor/magento/module-customer/etc/frontend/sections.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove or merge section dependencies from custom modules that trigger unnecessary invalidations:&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;!-- Your module's etc/frontend/sections.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;config&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;action&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"your/controller/action"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"cart"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Only include sections you actually modify --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/action&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;h3&gt;
  
  
  Preload Shipping Rates
&lt;/h3&gt;

&lt;p&gt;If your store has a predictable default shipping address (e.g., country-level), pre-populate the shipping estimator with defaults to avoid an extra round trip when the customer first opens checkout:&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 ShippingInformationManagement&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;afterSaveAddressInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$subject&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;$cartId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$addressInformation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Cache shipping rates per region/country for faster subsequent loads&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GraphQL for Checkout (Hyva / PWA)
&lt;/h3&gt;

&lt;p&gt;If you're running Hyva Checkout or a PWA frontend, the GraphQL-based checkout is significantly faster than the default KnockoutJS checkout because it eliminates the RequireJS overhead entirely. Hyva Checkout reports 60–80% faster Time to Interactive compared to Luma checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Database Optimization for Quote Operations
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;quote&lt;/code&gt; and &lt;code&gt;quote_item&lt;/code&gt; tables are write-heavy and often the source of checkout slowness.&lt;/p&gt;

&lt;h3&gt;
  
  
  Index Maintenance
&lt;/h3&gt;

&lt;p&gt;Ensure these tables are regularly optimized:&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;-- Check table fragmentation&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_free&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_length&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="s1"&gt;'your_db'&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="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'quote'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'quote_item'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'quote_address'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'quote_address_rate'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Optimize fragmented tables&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;quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quote_item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quote_address&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Clean Up Expired Quotes
&lt;/h3&gt;

&lt;p&gt;Old quotes bloat these tables and slow down queries. Set up a cron to purge them:&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 cron:run &lt;span class="nt"&gt;--group&lt;/span&gt; index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure quote lifetime in &lt;strong&gt;Admin &amp;gt; Stores &amp;gt; Configuration &amp;gt; Sales &amp;gt; Checkout &amp;gt; Shopping Cart &amp;gt; Quote Lifetime (days)&lt;/strong&gt;. A value of 30 days is usually sufficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Missing Indexes
&lt;/h3&gt;

&lt;p&gt;Some Magento installations are missing useful composite indexes on quote-related tables. Add them via a data patch:&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;moduleDataSetup&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getConnection&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;addIndex&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;moduleDataSetup&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'quote'&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;moduleDataSetup&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getIdxName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'quote'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'is_active'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'is_active'&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. Payment Method Optimization
&lt;/h2&gt;

&lt;p&gt;Payment methods are one of the biggest hidden performance killers in checkout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audit Third-Party Payment Extensions
&lt;/h3&gt;

&lt;p&gt;Every payment provider adds its own JS SDK to the checkout. Audit what's loading:&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 which payment methods are active&lt;/span&gt;
bin/magento config:show payment | &lt;span class="nb"&gt;grep &lt;/span&gt;active
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disable payment methods you don't use. Each inactive-but-installed method still loads its configuration in checkout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load Payment SDKs Lazily
&lt;/h3&gt;

&lt;p&gt;Payment SDKs (Stripe, PayPal, Mollie, etc.) should be loaded only when the customer reaches the payment step, not on initial page load. Most modern payment extensions support this — check your provider's configuration for "lazy load" or "defer script loading" options.&lt;/p&gt;

&lt;p&gt;For PayPal specifically:&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;!-- Disable PayPal in-context checkout if not needed --&amp;gt;&lt;/span&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;paypal&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;wpp_usuk&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;in_context&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/in_context&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/wpp_usuk&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/paypal&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;h2&gt;
  
  
  6. Server-Side Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  PHP Session Storage
&lt;/h3&gt;

&lt;p&gt;Checkout is session-heavy. Storing sessions in files (the default) creates I/O contention under load. Move sessions to Redis:&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;// app/etc/env.php&lt;/span&gt;
&lt;span class="s1"&gt;'session'&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;'save'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'redis'&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;'host'&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;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6379'&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="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2.5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'persistent_identifier'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'compression_threshold'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2048'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'compression_library'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'gzip'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'log_level'&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;span class="s1"&gt;'max_concurrency'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'break_after_frontend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'break_after_adminhtml'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'30'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'first_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'600'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'bot_first_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'60'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'bot_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'7200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'disable_locking'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'min_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'60'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'max_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2592000'&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;
  
  
  Enable HTTP/2
&lt;/h3&gt;

&lt;p&gt;HTTP/2 multiplexing significantly reduces the overhead of multiple concurrent AJAX requests during checkout. Ensure your Nginx/Apache configuration has HTTP/2 enabled:&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;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Measure Before and After
&lt;/h2&gt;

&lt;p&gt;Don't optimize blindly. Use these tools to benchmark:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chrome DevTools → Network tab&lt;/strong&gt; — Record a checkout flow and analyze waterfall&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebPageTest.org&lt;/strong&gt; — Test from different locations with filmstrip view&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New Relic / Blackfire&lt;/strong&gt; — Profile server-side checkout AJAX endpoints (&lt;code&gt;/shipping-information&lt;/code&gt;, &lt;code&gt;/payment-information&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Magento Profiler&lt;/strong&gt; — Enable with &lt;code&gt;bin/magento dev:profiler:enable&lt;/code&gt; in staging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key metrics to track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time to First Byte (TTFB)&lt;/strong&gt; on checkout page load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to Interactive (TTI)&lt;/strong&gt; — when the customer can actually interact&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total JS payload&lt;/strong&gt; on checkout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Number of AJAX calls&lt;/strong&gt; during a full checkout flow&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Before diving into complex optimizations, run through these quick wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] JS/CSS merging and minification enabled&lt;/li&gt;
&lt;li&gt;[ ] Expired quotes cleaned up (quote table &amp;lt; 500k rows)&lt;/li&gt;
&lt;li&gt;[ ] Sessions stored in Redis, not files&lt;/li&gt;
&lt;li&gt;[ ] Unused payment methods disabled&lt;/li&gt;
&lt;li&gt;[ ] FPC enabled and warming regularly&lt;/li&gt;
&lt;li&gt;[ ] HTTP/2 enabled on your server&lt;/li&gt;
&lt;li&gt;[ ] PHP OPcache configured correctly (see our &lt;a href="https://dev.to/blog/magento-2-php-opcache-tuning"&gt;OPcache guide&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Third-party checkout extensions audited for JS bloat&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Checkout performance is not a one-time fix — it's an ongoing practice. As you add payment providers, shipping integrations, and promotional logic, each one can quietly degrade the experience for your customers.&lt;/p&gt;

&lt;p&gt;Start with the measurement, identify your biggest bottleneck (usually JS payload or AJAX calls), and work systematically down the list. Even a 30% improvement in checkout speed can meaningfully impact your conversion rate and revenue.&lt;/p&gt;

&lt;p&gt;For stores on Hyva or PWA frontends, the path is even clearer: the component-based architecture makes it far easier to lazy-load and defer non-critical checkout functionality. If you're still on Luma and dealing with severe checkout slowness, migrating to Hyva Checkout is worth serious consideration.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>MySQL Query Cache vs Magento Cache: What's the Difference and When to Use Each</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Wed, 06 May 2026 09:03:01 +0000</pubDate>
      <link>https://dev.to/magevanta/mysql-query-cache-vs-magento-cache-whats-the-difference-and-when-to-use-each-1p46</link>
      <guid>https://dev.to/magevanta/mysql-query-cache-vs-magento-cache-whats-the-difference-and-when-to-use-each-1p46</guid>
      <description>&lt;p&gt;Caching is one of the most effective levers you have for speeding up a Magento 2 store. But "caching" is not a single thing — it's a stack of overlapping layers, each operating at a different level of your infrastructure. Two layers that often cause confusion are &lt;strong&gt;MySQL's query cache&lt;/strong&gt; and &lt;strong&gt;Magento's built-in cache&lt;/strong&gt;. They sound similar, they both exist to serve data faster, but they work in completely different ways and serve completely different purposes.&lt;/p&gt;

&lt;p&gt;This post breaks down exactly what each one does, where they overlap, where they don't, and how to configure them properly for a production Magento 2 environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is MySQL Query Cache?
&lt;/h2&gt;

&lt;p&gt;MySQL's query cache is a server-level feature that stores the result set of a &lt;code&gt;SELECT&lt;/code&gt; query alongside the raw SQL string. If the exact same query comes in again — character for character — MySQL returns the cached result without re-executing the query against the tables.&lt;/p&gt;

&lt;p&gt;It sounds like a win, but the reality is more nuanced.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;code&gt;SELECT&lt;/code&gt; query arrives at MySQL.&lt;/li&gt;
&lt;li&gt;MySQL hashes the query string and checks the query cache.&lt;/li&gt;
&lt;li&gt;If there's a hit, the result is returned immediately.&lt;/li&gt;
&lt;li&gt;If there's a miss, the query executes, and the result is stored in the cache.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key problem: &lt;strong&gt;any write to a table invalidates all cached queries that reference that table&lt;/strong&gt;. In a busy Magento store, tables like &lt;code&gt;sales_order&lt;/code&gt;, &lt;code&gt;catalog_product_entity&lt;/code&gt;, &lt;code&gt;quote&lt;/code&gt;, and &lt;code&gt;cataloginventory_stock_item&lt;/code&gt; are written to constantly. Every new order, every cart update, every stock decrement — all of these flush cached query results.&lt;/p&gt;

&lt;h3&gt;
  
  
  MySQL 8.0 removed it entirely
&lt;/h3&gt;

&lt;p&gt;MySQL 8.0 deprecated and removed the query cache. The MySQL team found that in high-concurrency environments, the query cache was actually a bottleneck due to the global mutex needed to manage cache invalidation. For Magento stores running MySQL 8.0 (which is now the standard), &lt;strong&gt;the query cache is simply not available&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you're still on MySQL 5.7, the query cache is present but disabled by default (&lt;code&gt;query_cache_type = 0&lt;/code&gt;). For most Magento workloads, leaving it disabled is the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Magento Cache?
&lt;/h2&gt;

&lt;p&gt;Magento's cache operates at the &lt;strong&gt;application layer&lt;/strong&gt;, not the database layer. Rather than caching SQL results, it caches serialized PHP objects, rendered HTML blocks, configuration arrays, and more. It uses a cache backend (File, Redis, Varnish) to store and retrieve these objects.&lt;/p&gt;

&lt;p&gt;Magento ships with several distinct cache types, each serving a specific purpose:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cache Type&lt;/th&gt;
&lt;th&gt;What It Stores&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Merged XML configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;layout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Page layout handles and block structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;block_html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rendered HTML of individual blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;collections&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;EAV collection results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reflection&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PHP class reflection data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db_ddl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Database table schema metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compiled_config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DI compiled configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;full_page&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Complete rendered page HTML (FPC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;translate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Translation strings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eav&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;EAV attribute metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each of these can be enabled or disabled independently:&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 cache:status
bin/magento cache:enable full_page
bin/magento cache:disable block_html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Full Page Cache
&lt;/h3&gt;

&lt;p&gt;The most impactful of these is &lt;code&gt;full_page&lt;/code&gt; — Magento's Full Page Cache (FPC). When a CMS page, category page, or product page is first rendered, the entire HTML response is stored. Subsequent requests for the same page serve the cached HTML without touching PHP or MySQL at all.&lt;/p&gt;

&lt;p&gt;FPC can be served by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Magento's built-in FPC&lt;/strong&gt; (file-based or Redis-based, ~50–200ms response)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Varnish&lt;/strong&gt; (reverse proxy, ~5–20ms response — the production-grade choice)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How They Interact
&lt;/h2&gt;

&lt;p&gt;Here's the key insight: &lt;strong&gt;these two caching layers are almost entirely independent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When Magento serves a cached full page, MySQL is not involved at all. When Magento's block cache is warm, it may skip several database queries entirely. Conversely, MySQL query cache (on 5.7) only kicks in when Magento actually executes a &lt;code&gt;SELECT&lt;/code&gt; — which it tries hard to avoid by using its own cache first.&lt;/p&gt;

&lt;p&gt;The interaction diagram looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
  └─► Varnish FPC hit? → Serve HTML (MySQL never touched)
        └─► Magento FPC hit? → Serve HTML (MySQL never touched)
              └─► Block cache / config cache hit? → Partial PHP execution
                    └─► MySQL query
                          └─► MySQL query cache hit? (5.7 only) → Return result
                                └─► Execute query against tables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, a warm Magento cache means MySQL sees dramatically fewer queries. The MySQL query cache, when it was still available, only helped at the very bottom of this chain — for queries that escaped all of Magento's own caching layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration Recommendations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  MySQL (5.7)
&lt;/h3&gt;

&lt;p&gt;If you're still on MySQL 5.7, keep the query cache disabled:&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/mysql/mysql.conf.d/mysqld.cnf
&lt;/span&gt;&lt;span class="py"&gt;query_cache_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;query_cache_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The write-invalidation behavior combined with Magento's write-heavy workload makes it a net negative in most cases. Spend the memory on &lt;code&gt;innodb_buffer_pool_size&lt;/code&gt; instead — that's where your MySQL performance gains are.&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;# For a server with 16GB RAM dedicated to MySQL
&lt;/span&gt;&lt;span class="py"&gt;innodb_buffer_pool_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10G&lt;/span&gt;
&lt;span class="py"&gt;innodb_buffer_pool_instances&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  MySQL (8.0)
&lt;/h3&gt;

&lt;p&gt;Nothing to configure — the query cache doesn't exist. Focus on InnoDB tuning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Magento Cache Backend
&lt;/h3&gt;

&lt;p&gt;For any production store, use &lt;strong&gt;Redis&lt;/strong&gt; as the cache backend. File-based caching is fine for development but falls apart under load due to filesystem contention.&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:config:set &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cache-backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redis &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cache-backend-redis-server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;127.0.0.1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cache-backend-redis-port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6379 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cache-backend-redis-db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the session cache, use a separate Redis instance or at least a separate database index:&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:config:set &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-save&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redis &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-save-redis-host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;127.0.0.1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-save-redis-port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6379 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-save-redis-db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Full Page Cache
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;app/etc/env.php&lt;/code&gt;, confirm FPC is set to Redis (or Varnish if applicable):&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;'cache'&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;'frontend'&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;'full_page'&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;'backend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Cm_Cache_Backend_Redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'backend_options'&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;'server'&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;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6379'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2'&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;For high-traffic stores, put Varnish in front:&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 &lt;span class="nt"&gt;--scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;default system/full_page_cache/caching_application 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quick Reference: Which Cache Layer to Focus On
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Recommended Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pages loading slowly, TTFB &amp;gt; 500ms&lt;/td&gt;
&lt;td&gt;Enable Varnish or Magento FPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL CPU spiking under load&lt;/td&gt;
&lt;td&gt;Check InnoDB buffer pool, not query cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow admin panel after deploy&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;bin/magento cache:flush&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query cache enabled on MySQL 5.7&lt;/td&gt;
&lt;td&gt;Disable it — likely a net negative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev environment with slow page loads&lt;/td&gt;
&lt;td&gt;Enable all Magento cache types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session-related slowness&lt;/td&gt;
&lt;td&gt;Move sessions to Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;MySQL query cache and Magento cache are two different tools solving different problems at different layers of the stack. For modern Magento 2 on MySQL 8.0, MySQL query cache is not a factor — it no longer exists. Your caching focus should be entirely on Magento's application cache, with Redis as the backend and Varnish (or Magento FPC) handling full-page responses.&lt;/p&gt;

&lt;p&gt;The real performance wins in Magento come from eliminating PHP and database execution altogether — not from making individual SQL queries slightly faster. Invest in the upper layers of the cache stack first, then tune the database buffer pool, and don't waste time chasing a query cache that either doesn't exist or actively hurts you.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>caching</category>
    </item>
    <item>
      <title>Magento 2 Customer Segments: The Hidden Performance Killer</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Tue, 05 May 2026 09:03:01 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-customer-segments-the-hidden-performance-killer-5c2a</link>
      <guid>https://dev.to/magevanta/magento-2-customer-segments-the-hidden-performance-killer-5c2a</guid>
      <description>&lt;p&gt;If you've ever profiled a slow Magento 2 store and found mysterious database queries eating hundreds of milliseconds on every page load, customer segments are often the culprit. They're powerful. They're flexible. And they're one of the most underestimated performance drains in the entire platform.&lt;/p&gt;

&lt;p&gt;This post dives deep into how customer segments work, why they're expensive, and what you can do about it without sacrificing the personalization they enable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Customer Segments?
&lt;/h2&gt;

&lt;p&gt;Magento 2's customer segments (Commerce/Enterprise edition only) let you target specific groups of customers based on dynamic conditions — order history, cart contents, addresses, wishlists, and more. They power targeted promotions, personalized banners, and cart price rules.&lt;/p&gt;

&lt;p&gt;Sounds great. The catch: &lt;strong&gt;segment membership is evaluated dynamically&lt;/strong&gt;, often on every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Segments Kill Performance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Evaluation Loop
&lt;/h3&gt;

&lt;p&gt;When a customer logs in — or sometimes even on anonymous requests — Magento re-evaluates which segments apply to that customer. Each segment can trigger one or more SQL queries. A store with 20–30 segments can easily fire 20–30 extra queries per page load.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;SHOW PROCESSLIST&lt;/code&gt; during peak traffic on an unoptimized store and you'll typically see a flood of queries like:&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="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="nv"&gt;`sales_order`&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;`customer_id`&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; 
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nv"&gt;`created_at`&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; 
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nv"&gt;`status`&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are segment condition checks. They're not cached by default. They run on every affected page.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;magento_customersegment_customer&lt;/code&gt; Table
&lt;/h3&gt;

&lt;p&gt;Magento stores resolved segment memberships in &lt;code&gt;magento_customersegment_customer&lt;/code&gt;. The problem: this table gets invalidated and repopulated aggressively. After certain events (order placed, cart updated, customer profile changed), segments are marked dirty and re-queued for recalculation.&lt;/p&gt;

&lt;p&gt;On high-traffic stores, this table can have millions of rows — and the recalculation jobs can stack up in the cron queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-time vs. Scheduled Processing
&lt;/h3&gt;

&lt;p&gt;By default, Magento evaluates segments in &lt;strong&gt;real-time&lt;/strong&gt; for logged-in customers. This is the worst mode for performance.&lt;/p&gt;

&lt;p&gt;Check your current setting:&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:show customer/magento_customersegment/is_enabled
bin/magento config:show customer/magento_customersegment/real_time_check_if_customer_is_logged_in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;real_time_check_if_customer_is_logged_in&lt;/code&gt; is &lt;code&gt;1&lt;/code&gt;, you're paying a performance tax on every authenticated page load.&lt;/p&gt;

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

&lt;p&gt;Before optimizing, measure the impact on your store.&lt;/p&gt;

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

&lt;p&gt;Add to &lt;code&gt;app/etc/env.php&lt;/code&gt; temporarily:&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;'db'&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;'connection'&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;'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\\DB\\Profiler'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'enabled'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;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;Then analyze your logs or use a tool like New Relic, Blackfire, or even &lt;code&gt;EXPLAIN&lt;/code&gt; on suspected queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Count Active Segments
&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="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;magento_customersegment_segment&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_active&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;More than 15–20 active segments? You're likely feeling the pain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check Segment Complexity
&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;segment_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;conditions_serialized&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;magento_customersegment_segment&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_active&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;Segments with deeply nested conditions or those checking order history are the most expensive. Segments checking only customer attributes (email domain, group) are cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization Strategies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Disable Real-Time Evaluation
&lt;/h3&gt;

&lt;p&gt;This is the single biggest win. Switch to scheduled segment processing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin → Stores → Configuration → Customers → Customer Segments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set &lt;strong&gt;"Real-time Check if Customer is Logged in"&lt;/strong&gt; to &lt;strong&gt;No&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Or via CLI:&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 customer/magento_customersegment/real_time_check_if_customer_is_logged_in 0
bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this off, segment membership is resolved via cron (&lt;code&gt;customer_segment_match_cron&lt;/code&gt;) rather than on every request. There's a small lag before new customers land in segments, but for most use cases this is completely acceptable.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Limit Active Segments
&lt;/h3&gt;

&lt;p&gt;Audit every segment. Ask: "Is this actually being used in a price rule or banner right now?"&lt;/p&gt;

&lt;p&gt;Disable unused segments immediately. Every active segment you remove eliminates queries per page load:&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;-- Check which segments are attached to price rules&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cs&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;cr&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;as&lt;/span&gt; &lt;span class="n"&gt;rule_name&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;magento_customersegment_segment&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;magento_customersegment_rule&lt;/span&gt; &lt;span class="n"&gt;cr&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;segment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;segment_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_active&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;Segments with no attached rules are dead weight. Disable or delete them.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Simplify Segment Conditions
&lt;/h3&gt;

&lt;p&gt;Refactor complex segments wherever possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Instead of:&lt;/strong&gt; "customer has placed more than 2 orders with total &amp;gt; €100 in last 90 days"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use:&lt;/strong&gt; A custom customer attribute (&lt;code&gt;is_loyal_customer = 1&lt;/code&gt;) updated nightly by a cron job&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pushing complexity from runtime evaluation to a scheduled batch process is almost always faster at request time.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Add Database Indexes
&lt;/h3&gt;

&lt;p&gt;If segments query order or quote data, make sure the relevant columns are indexed:&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;-- Check for missing indexes on columns used in segment conditions&lt;/span&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;sales_order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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;quote&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common columns worth indexing if missing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sales_order.customer_id&lt;/code&gt; + &lt;code&gt;created_at&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sales_order.status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;quote.customer_id&lt;/code&gt; + &lt;code&gt;updated_at&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Tune the Cron Job
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;customer_segment_match_cron&lt;/code&gt; job processes segment recalculations. Make sure it's running regularly but not constantly re-processing the entire customer base.&lt;/p&gt;

&lt;p&gt;Check your cron group configuration and consider moving segment processing to off-peak hours:&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;!-- Custom cron schedule example --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;group&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"customer_segment"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;schedule_generate_every&amp;gt;&lt;/span&gt;15&lt;span class="nt"&gt;&amp;lt;/schedule_generate_every&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;schedule_ahead_for&amp;gt;&lt;/span&gt;20&lt;span class="nt"&gt;&amp;lt;/schedule_ahead_for&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;schedule_lifetime&amp;gt;&lt;/span&gt;15&lt;span class="nt"&gt;&amp;lt;/schedule_lifetime&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;history_cleanup_every&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/history_cleanup_every&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;history_success_lifetime&amp;gt;&lt;/span&gt;60&lt;span class="nt"&gt;&amp;lt;/history_success_lifetime&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;history_failure_lifetime&amp;gt;&lt;/span&gt;600&lt;span class="nt"&gt;&amp;lt;/history_failure_lifetime&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;use_separate_process&amp;gt;&lt;/span&gt;1&lt;span class="nt"&gt;&amp;lt;/use_separate_process&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/group&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running segment cron in a separate process prevents it from blocking other critical jobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Cache Segment Results Manually
&lt;/h3&gt;

&lt;p&gt;For stores with custom segment conditions, consider caching the result in Redis with a TTL:&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;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer_segments_'&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="o"&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;$segments&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;segmentHelper&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCustomerSegmentIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$customer&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;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$segments&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="nc"&gt;\Magento\Customer\Model\Customer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CACHE_TAG&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="c1"&gt;// 1 hour TTL&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 custom code but can be a lifesaver on high-traffic stores where even scheduled evaluation is too slow.&lt;/p&gt;

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

&lt;p&gt;After applying these optimizations, benchmark the impact:&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;# Use siege or ab for load testing&lt;/span&gt;
siege &lt;span class="nt"&gt;-c&lt;/span&gt; 10 &lt;span class="nt"&gt;-t&lt;/span&gt; 30S https://yourstore.com/customer/account/

&lt;span class="c"&gt;# Check slow query log&lt;/span&gt;
mysql &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SHOW VARIABLES LIKE 'slow_query_log%';"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In real-world cases we've seen, disabling real-time evaluation alone reduced page load time by &lt;strong&gt;200–400ms&lt;/strong&gt; on stores with 20+ segments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Right Architecture
&lt;/h2&gt;

&lt;p&gt;The safest pattern for customer segments at scale:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep real-time evaluation off&lt;/strong&gt; — always&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch-process memberships nightly&lt;/strong&gt; or after major events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use customer attributes as proxies&lt;/strong&gt; for expensive conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit segments quarterly&lt;/strong&gt; — disable anything that isn't earning its keep&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor the cron queue&lt;/strong&gt; — a backed-up segment queue is a silent killer&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Customer segments are not inherently bad — they're a legitimate tool for personalization and targeted promotions. But like any powerful feature, they need to be wielded carefully.&lt;/p&gt;

&lt;p&gt;The default Magento configuration prioritizes correctness (real-time evaluation) over performance. For most stores, the opposite trade-off is better: accept a few minutes of lag in segment membership in exchange for dramatically faster page loads.&lt;/p&gt;

&lt;p&gt;Disable real-time evaluation, prune your inactive segments, and simplify complex conditions. Your TTFB will thank you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Running Magento on infrastructure you don't fully control? Check out our &lt;a href="https://dev.to/blog/magento-2-performance-optimization-guide-2026"&gt;performance optimization guide&lt;/a&gt; for a complete picture.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
