<?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.us-east-2.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 Cart Price Rules Performance: Optimize Complex Promotions at Scale</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Wed, 17 Jun 2026 09:02:23 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-cart-price-rules-performance-optimize-complex-promotions-at-scale-m9b</link>
      <guid>https://dev.to/magevanta/magento-2-cart-price-rules-performance-optimize-complex-promotions-at-scale-m9b</guid>
      <description>&lt;p&gt;Cart price rules are one of Magento's most powerful marketing features — and one of the fastest ways to tank your store's performance if you're not careful.&lt;/p&gt;

&lt;p&gt;When you have hundreds of active rules, millions of coupon codes, or complex conditions spanning multiple product attributes, every cart update can trigger an expensive rule validation cycle that turns a smooth checkout into a sluggish mess.&lt;/p&gt;

&lt;p&gt;In this guide, I'll walk you through why cart price rules slow things down, how to measure the impact, and what you can do about it — from rule design and indexing to caching and database optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cart Price Rules Are Expensive
&lt;/h2&gt;

&lt;p&gt;Every time a customer adds, removes, or updates an item in their cart, Magento re-validates all active cart price rules. Here's what happens under the hood:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;All active rules are loaded&lt;/strong&gt; — Magento fetches every active rule from &lt;code&gt;salesrule&lt;/code&gt; and related tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditions are evaluated&lt;/strong&gt; — Each rule's conditions are run against the current quote, which means loading product data, customer group data, and sometimes even address/category data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coupon codes are checked&lt;/strong&gt; — If a coupon is present, Magento validates it against the &lt;code&gt;salesrule_coupon&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free shipping &amp;amp; discounts are computed&lt;/strong&gt; — Applied amounts are recalculated for every quote item&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The result is cached per quote&lt;/strong&gt; — But the cache is invalidated on every cart change&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The performance impact grows exponentially with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of active rules&lt;/li&gt;
&lt;li&gt;Complexity of conditions per rule&lt;/li&gt;
&lt;li&gt;Number of items in the cart&lt;/li&gt;
&lt;li&gt;Number of coupon codes in circulation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Measuring Rule Validation Time
&lt;/h2&gt;

&lt;p&gt;Before optimizing, measure your baseline. The quickest way is to check the &lt;code&gt;salesrule&lt;/code&gt; validator execution time using a simple profiling 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/Module/Plugin/TimingPlugin.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;aroundProcess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;\Magento\SalesRule\Model\Validator&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;callable&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$address&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;200&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 slow rules — identify which rule IDs trigger long runs&lt;/span&gt;
        &lt;span class="nc"&gt;Mage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SalesRule validation took &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$elapsed&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Zend_Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WARN&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;You can also check MySQL's slow query log for the &lt;code&gt;salesrule_coupon&lt;/code&gt; and &lt;code&gt;salesrule&lt;/code&gt; queries:&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;-- Enable slow query logging temporarily&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;slow_query_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;long_query_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- 1 second&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After collecting data, look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queries on &lt;code&gt;salesrule_coupon&lt;/code&gt; taking &amp;gt;500ms&lt;/li&gt;
&lt;li&gt;Multiple sequential rule condition evaluations per cart update&lt;/li&gt;
&lt;li&gt;High query count for &lt;code&gt;salesrule_customer_group&lt;/code&gt;, &lt;code&gt;salesrule_website&lt;/code&gt;, &lt;code&gt;salesrule_label&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rule Design Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Combine Rules Instead of Duplicating
&lt;/h3&gt;

&lt;p&gt;The single biggest mistake is creating one rule per product, category, or customer segment. If you have 500 rules for 500 products, Magento evaluates every single one on every cart change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instead:&lt;/strong&gt; Use wildcard conditions or combine rules using SQL-level conditions. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ Bad: One rule per product SKU (500 rules)
✅ Good: One rule with condition "SKU starts with PROMO-" + category condition (1 rule)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you genuinely need per-product rules, consider whether a catalog price rule or tier pricing would work instead — these are evaluated at index time, not checkout time.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Keep Conditions Simple
&lt;/h3&gt;

&lt;p&gt;Each condition in Magento's rule engine adds a JOIN or subquery to the evaluation SQL. A rule with 10 conditions can generate a query with 10+ JOINs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Priority of condition types (from cheapest to most expensive):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Customer group&lt;/strong&gt; — Single indexed column lookup (cheapest)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt; — Same, indexed FK&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grand total / Subtotal&lt;/strong&gt; — Simple numeric comparison&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Number of items&lt;/strong&gt; — COUNT query, relatively cheap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SKU&lt;/strong&gt; — Can be expensive if using array conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Category&lt;/strong&gt; — Requires EAV category path lookup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attribute conditions&lt;/strong&gt; — Can involve EAV joins, very expensive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subselection conditions&lt;/strong&gt; — Most expensive, nested queries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; Put the cheapest conditions first and use as few as possible. A rule with just "customer group = wholesale" + "subtotal &amp;gt; €100" will be evaluated in milliseconds. A rule with 8 attribute conditions and a category subselection can take seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Avoid "All Items" Conditions Where Possible
&lt;/h3&gt;

&lt;p&gt;Conditions that say "If ALL of these conditions are TRUE" (vs "If ANY") trigger full cart iteration. For stores with 50+ item carts, this adds up fast.&lt;/p&gt;

&lt;p&gt;If you need per-item conditions, make sure they're on indexed attributes (SKU, category path) and keep the total under 5 conditions per rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coupon Code Performance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Coupon Generation at Scale
&lt;/h3&gt;

&lt;p&gt;Generating a few thousand coupons is fine. Generating a few million — which happens with large email campaigns — requires careful planning.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;salesrule_coupon&lt;/code&gt; table stores every single generated code. When a customer enters a coupon code, Magento searches this table with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;salesrule_coupon&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expiration_date&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;expiration_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without proper indexing, this query scans the entire table. &lt;strong&gt;Always ensure you have these indexes:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check existing indexes&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;salesrule_coupon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Essential: code + rule_id compound index&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;salesrule_coupon&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IDX_SR_COUPON_CODE_RULE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rule_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- If using expiration dates frequently&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;salesrule_coupon&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IDX_SR_COUPON_EXPIRATION&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expiration_date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Coupon Code Prefix Strategy
&lt;/h3&gt;

&lt;p&gt;Use &lt;strong&gt;coupon prefixes&lt;/strong&gt; instead of generating millions of individual codes. Magento supports coupon code prefixes natively in the admin panel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Instead of generating 100,000 individual codes like &lt;code&gt;SUMMER-00001&lt;/code&gt; through &lt;code&gt;SUMMER-100000&lt;/code&gt;, create one rule with coupon prefix &lt;code&gt;SUMMER-&lt;/code&gt; and let Magento validate the pattern rather than looking up individual codes.&lt;/p&gt;

&lt;p&gt;This cuts the &lt;code&gt;salesrule_coupon&lt;/code&gt; table from millions of rows to just one.&lt;/p&gt;

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

&lt;p&gt;Set proper expiration dates on all coupon-based rules and clean up expired codes periodically:&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;-- Archive expired coupons (run weekly via cron)&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;salesrule_coupon&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;expiration_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;times_used&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep coupons that have been used — you need those for order history integrity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Indexing &amp;amp; Database Optimization
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Rule Index Tables
&lt;/h3&gt;

&lt;p&gt;Magento's &lt;code&gt;salesrule_product_attribute&lt;/code&gt; table is a flat index mapping rules → products → attributes. When this table grows large, it can slow down attribute-based conditions.&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 its size&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;as&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
       &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;data_length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;index_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;size_mb&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'salesrule_product_attribute'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this table exceeds 100MB, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reducing number of attribute-based conditions&lt;/li&gt;
&lt;li&gt;Removing old/expired rules from the index&lt;/li&gt;
&lt;li&gt;MariaDB performance: Partitioning this table by &lt;code&gt;rule_id&lt;/code&gt; or &lt;code&gt;attribute_id&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Newsletter Coupon Tables
&lt;/h3&gt;

&lt;p&gt;If you use Magento's newsletter coupon functionality, the &lt;code&gt;newsletter_problem&lt;/code&gt; and related tables can bloat. Clean them during off-peak hours:&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="n"&gt;OPTIMIZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;salesrule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;OPTIMIZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;salesrule_coupon&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;OPTIMIZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;salesrule_product_attribute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;OPTIMIZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;salesrule_customer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;OPTIMIZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;salesrule_customer_group&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;OPTIMIZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;salesrule_website&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Caching Strategies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cart Price Rule Cache
&lt;/h3&gt;

&lt;p&gt;Magento 2 has built-in caching for rule validation results through the &lt;code&gt;validate&lt;/code&gt; cache type and the quote's &lt;code&gt;trigger_recollect&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key tip:&lt;/strong&gt; Disable the &lt;code&gt;recollect&lt;/code&gt; trigger for every single cart page load if you're not displaying discount changes live:&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;!-- etc/frontend/di.xml --&amp;gt;&lt;/span&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\Quote\Model\Quote"&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;"disable_recollect_on_load"&lt;/span&gt; 
            &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"Vendor\Module\Plugin\DisableQuoteRecollect"&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 prevents the full rule validation from running on every &lt;code&gt;GET&lt;/code&gt; cart page load — it only runs when the cart actually changes.&lt;/p&gt;

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

&lt;p&gt;Cart price rules are dynamic content — they can't be cached in FPC. However, you can use &lt;strong&gt;ESI (Edge Side Includes)&lt;/strong&gt; or &lt;strong&gt;Varnish hole-punching&lt;/strong&gt; to isolate the cart price rule output:&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="c1"&gt;# In varnish.vcl — only if your theme supports it&lt;/span&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.url&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s2"&gt;"^/checkout/cart/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;# Don't cache the cart page — rules are dynamic&lt;/span&gt;
        &lt;span class="nf"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;pass&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;Alternatively, if you use a &lt;strong&gt;microservice approach&lt;/strong&gt; for cart calculations (like Vue Storefront or PWA), move price rule computation to a dedicated service that can handle the load independently.&lt;/p&gt;

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

&lt;p&gt;Here's what a real Magento store achieved after applying these optimizations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Optimization&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rule conditions per rule&lt;/td&gt;
&lt;td&gt;8-12&lt;/td&gt;
&lt;td&gt;3-4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active rules&lt;/td&gt;
&lt;td&gt;340&lt;/td&gt;
&lt;td&gt;42 (combined)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coupon codes&lt;/td&gt;
&lt;td&gt;2.1M&lt;/td&gt;
&lt;td&gt;14K (prefix pattern)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cart validation time (10 items)&lt;/td&gt;
&lt;td&gt;2.3s&lt;/td&gt;
&lt;td&gt;280ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout page load&lt;/td&gt;
&lt;td&gt;4.1s&lt;/td&gt;
&lt;td&gt;1.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest win? Combining 300+ individual product rules into three category-based rules, and switching to coupon prefixes.&lt;/p&gt;

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

&lt;p&gt;Here's your quick action plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Audit&lt;/strong&gt; — Count active rules, measure validation time with a profiling plugin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combine&lt;/strong&gt; — Merge rules with similar conditions; prefer catalog price rules where possible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplify&lt;/strong&gt; — Reduce condition complexity, use cheapest condition types first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index&lt;/strong&gt; — Verify &lt;code&gt;salesrule_coupon&lt;/code&gt; indexes; add compound indexes if missing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefix&lt;/strong&gt; — Use coupon code prefixes instead of bulk generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Purge&lt;/strong&gt; — Clean expired, unused coupons regularly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache&lt;/strong&gt; — Minimize &lt;code&gt;trigger_recollect&lt;/code&gt; on cart page views&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor&lt;/strong&gt; — Keep an eye on &lt;code&gt;salesrule_product_attribute&lt;/code&gt; table size&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cart price rules don't have to be a performance nightmare. By designing rules thoughtfully, indexing properly, and cleaning up regularly, you can run hundreds of promotions without slowing down a single checkout.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Large Catalog Performance: Scaling Beyond 100K Products</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Mon, 15 Jun 2026 09:13:40 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-large-catalog-performance-scaling-beyond-100k-products-1472</link>
      <guid>https://dev.to/magevanta/magento-2-large-catalog-performance-scaling-beyond-100k-products-1472</guid>
      <description>&lt;p&gt;Running Magento 2 with tens of thousands of products is one thing. Crossing the 100K threshold — or pushing toward a million SKUs — is an entirely different ballgame. The architecture that hums along happily at 10K products starts showing cracks: category pages that take 5+ seconds, layered navigation that times out, indexers that run for hours, and an admin panel that's barely usable.&lt;/p&gt;

&lt;p&gt;The good news? Magento 2 can absolutely handle catalogs of this size. It just needs the right configuration, the right extensions, and the right approach. Here's what actually matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The EAV Bottleneck: Your First Wall
&lt;/h2&gt;

&lt;p&gt;Magento 2's EAV (Entity-Attribute-Value) data model is flexible but brutal at scale. Every product page load can generate dozens of JOIN queries against &lt;code&gt;catalog_product_entity_*&lt;/code&gt; tables. At 10K products this is fine. At 200K, it becomes a database killer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, evaluate which attributes genuinely need to be searchable, filterable, or visible in product listings. Every attribute flagged as "Filterable" or "Used in Product Listing" adds weight to the EAV queries. In a large catalog, only the attributes your customers actually use should be enabled.&lt;/p&gt;

&lt;p&gt;Second, use the &lt;code&gt;catalog_product_flat&lt;/code&gt; tables — but with caution. Flat tables denormalize EAV data into a single row per product, which can dramatically speed up category and listing queries. However, they come with trade-offs: longer reindex times, increased storage, and complexity with multi-store setups. For catalogs over 500K products, flat tables may actually hurt more than help — test aggressively.&lt;/p&gt;

&lt;p&gt;Third, consider moving attribute-heavy lookups to Elasticsearch or OpenSearch. The search engine handles faceted navigation far more efficiently than MySQL EAV queries. This is the single biggest win for large catalogs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Category Tree Navigation at Scale
&lt;/h2&gt;

&lt;p&gt;Category pages are often the most visited pages on a Magento store. With a large catalog, rendering a category tree with thousands of products and deep nesting becomes expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key optimizations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Limit category depth.&lt;/strong&gt; Deeply nested categories (5+ levels) generate recursive queries that don't scale. Flatten your tree where possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use full-page caching aggressively.&lt;/strong&gt; Category pages are cacheable by default. Ensure your TTL is reasonable and that cache warming covers your top 100-200 categories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anchor categories are expensive.&lt;/strong&gt; An anchor category inherits products from all subcategories. With large catalogs, an anchor category near the root can represent 80% of your catalog. Use anchor sparingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consider pagination size.&lt;/strong&gt; 24 products per page means more page loads and more queries. For B2B catalogs, 48-96 per page can reduce server load. Don't offer "Show All" — it will crash.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Layered Navigation: The Performance Killer
&lt;/h2&gt;

&lt;p&gt;Layered navigation (filterable attributes on category pages) is often the #1 performance problem in large catalogs. The default Magento implementation queries the EAV index tables, which can be catastrophically slow with 200K+ products.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solutions ranked by effectiveness:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Move to Elasticsearch/OpenSearch&lt;/strong&gt; — The search engine handles aggregations natively, fast. This is non-negotiable for large catalogs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable "Use in Search Results Layered Navigation"&lt;/strong&gt; instead of "Use in Layered Navigation" for attributes that don't need to show on category pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limit the number of filterable attributes.&lt;/strong&gt; 10-15 is plenty. Every extra attribute is an additional aggregation query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price navigation is especially expensive.&lt;/strong&gt; The dynamic price range calculation scans the entire price index. Consider using fixed price ranges instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache the layered navigation state&lt;/strong&gt; with a dedicated Redis cache for catalog layer data.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Price Calculation and Tier Prices
&lt;/h2&gt;

&lt;p&gt;Price calculations at scale involve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Base price, special price, tier prices, catalog rules, customer group prices, and tax rules&lt;/li&gt;
&lt;li&gt;All computed across potentially hundreds of thousands of products&lt;/li&gt;
&lt;li&gt;Usually during index operations but sometimes on-the-fly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For large catalogs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid complex catalog price rules. Each rule is evaluated against every applicable product during indexing. A single "Buy X get Y" rule with conditions can triple index time.&lt;/li&gt;
&lt;li&gt;Prefer tier prices and customer group prices over catalog rules when possible. They're evaluated at the product level, not through rule logic.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;catalog_product_price&lt;/code&gt; indexer in "Schedule" mode, never "Update by Schedule" — and monitor its runtime carefully.&lt;/li&gt;
&lt;li&gt;Consider splitting complex catalogs: base prices indexed normally, dynamic pricing handled through a custom module or PPC (Per-Price-Cache) extension.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Indexer Strategy for Large Catalogs
&lt;/h2&gt;

&lt;p&gt;This is where most large-catalog Magento stores fail. Default indexer settings don't account for scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Critical rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All indexers should be "Update by Schedule"&lt;/strong&gt; — never "Update on Save" for catalogs over 50K products.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stagger indexer runs.&lt;/strong&gt; Don't run all indexers simultaneously. Create a cron schedule that runs them sequentially.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor indexer table sizes.&lt;/strong&gt; The &lt;code&gt;catalog_product_index_price&lt;/code&gt; and &lt;code&gt;catalogrule_product&lt;/code&gt; tables grow linearly with products × customer groups × websites. At 200K products × 5 websites × 10 customer groups, you're at 10M rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the &lt;code&gt;--batch-size&lt;/code&gt; option&lt;/strong&gt; to control memory. Default batch is 1000; for very large catalogs, 500 may be safer to avoid memory spikes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consider incremental indexing where possible.&lt;/strong&gt; Some indexers support partial reindexing based on change log tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Search Engine Scaling
&lt;/h2&gt;

&lt;p&gt;Elasticsearch or OpenSearch is mandatory for any large Magento catalog. But even search engines need tuning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tips for search at scale:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Allocate enough heap.&lt;/strong&gt; A 500K product catalog with 30 attributes indexed needs at least 8GB-16GB of JVM heap. Don't run on defaults.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize mapping.&lt;/strong&gt; Only index attributes that are actually used in search, filters, or sorting. Each indexed attribute increases index size and query time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use index sharding wisely.&lt;/strong&gt; For a single Magento store, 3-5 shards per index is usually enough. Too many shards fragment the index and hurt performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh interval matters.&lt;/strong&gt; The default 1-second refresh interval in ES writes continuously. For Magento, 30-60 seconds is usually fine and reduces I/O pressure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consider a dedicated search cluster&lt;/strong&gt; for very large catalogs (&amp;gt;1M products). Running ES on the same server as MySQL and PHP is a recipe for resource contention.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Database-Level Optimizations
&lt;/h2&gt;

&lt;p&gt;Your MySQL configuration needs to change at scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Increase &lt;code&gt;innodb_buffer_pool_size&lt;/code&gt;&lt;/strong&gt; to 70-80% of available RAM. For a 500K product catalog with complex EAV, the working set easily reaches 8-16GB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable &lt;code&gt;innodb_parallel_read_threads&lt;/code&gt;&lt;/strong&gt; (MySQL 8+) to parallelize EAV JOIN queries. This alone can cut category page load times by 30-40%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partition large tables.&lt;/strong&gt; The &lt;code&gt;catalog_product_index_price&lt;/code&gt; and &lt;code&gt;catalogrule_product&lt;/code&gt; tables are excellent candidates for MySQL partitioning by website or customer group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor slow queries.&lt;/strong&gt; Magento generates some inherently slow queries at scale. Use the MySQL slow query log to find them and consider query rewrites via plugins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection pooling&lt;/strong&gt; becomes critical. At 200+ concurrent admin + frontend users, Magento's default connection handling creates too many connections. Use a connection pooler like ProxySQL or PHP-FPM's persistent connections.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Admin Panel Performance
&lt;/h2&gt;

&lt;p&gt;A large catalog doesn't just affect customers — your team suffers too. The admin product grid with 200K products is painful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin-specific optimizations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Limit the admin product grid&lt;/strong&gt; — don't show all products by default. Set default pagination to 20 and disable "Show All" for grids.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use admin mass actions&lt;/strong&gt; for bulk updates instead of the product grid. Direct grid editing generates individual AJAX saves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create admin roles with scoped catalog access&lt;/strong&gt; — if your team only manages a subset of products, give them filtered access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consider a headless admin&lt;/strong&gt; or dedicated PIM integration for very large catalogs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-World Results
&lt;/h2&gt;

&lt;p&gt;Here's what a properly tuned Magento 2 store with 350K SKUs achieved:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Optimization&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Category page (anchor, 40K products)&lt;/td&gt;
&lt;td&gt;8.2s&lt;/td&gt;
&lt;td&gt;1.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layered navigation with 12 filters&lt;/td&gt;
&lt;td&gt;6.7s&lt;/td&gt;
&lt;td&gt;0.9s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price indexer runtime&lt;/td&gt;
&lt;td&gt;47 min&lt;/td&gt;
&lt;td&gt;12 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search result page&lt;/td&gt;
&lt;td&gt;3.1s&lt;/td&gt;
&lt;td&gt;0.6s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin product grid load&lt;/td&gt;
&lt;td&gt;12s&lt;/td&gt;
&lt;td&gt;2.3s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key wasn't any single change — it was the combination of ES-powered navigation, sequential indexers, database tuning, and aggressive caching.&lt;/p&gt;

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

&lt;p&gt;☐ Move all layered navigation to Elasticsearch/OpenSearch&lt;br&gt;&lt;br&gt;
☐ Set all indexers to "Update by Schedule"&lt;br&gt;&lt;br&gt;
☐ Stagger indexer cron jobs with different intervals&lt;br&gt;&lt;br&gt;
☐ Increase MySQL buffer pool to 70-80% of RAM&lt;br&gt;&lt;br&gt;
☐ Limit filterable attributes to 10-15&lt;br&gt;&lt;br&gt;
☐ Avoid complex catalog price rules&lt;br&gt;&lt;br&gt;
☐ Use tier prices and group prices instead&lt;br&gt;&lt;br&gt;
☐ Limit category depth (max 3-4 levels)&lt;br&gt;&lt;br&gt;
☐ Increase search engine heap allocation&lt;br&gt;&lt;br&gt;
☐ Tune ES/OpenSearch refresh interval to 30-60s&lt;br&gt;&lt;br&gt;
☐ Test flat tables vs EAV at your specific scale&lt;br&gt;&lt;br&gt;
☐ Monitor indexer times daily&lt;br&gt;&lt;br&gt;
☐ Restrict admin grid defaults  &lt;/p&gt;

&lt;p&gt;Large catalog performance isn't a single fix — it's a strategy. Start with the search engine migration, then work through the indexer and database layers. Each optimization compounds, and before you know it, that 300K catalog runs faster than your competitor's 10K one.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>performance</category>
      <category>scaling</category>
    </item>
    <item>
      <title>Magento 2 EAV Performance Deep Dive: Optimizing the Entity-Attribute-Value Model</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 14 Jun 2026 09:02:57 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-eav-performance-deep-dive-optimizing-the-entity-attribute-value-model-od9</link>
      <guid>https://dev.to/magevanta/magento-2-eav-performance-deep-dive-optimizing-the-entity-attribute-value-model-od9</guid>
      <description>&lt;p&gt;The Entity-Attribute-Value (EAV) model is both Magento's greatest strength and its most persistent performance bottleneck. It gives you unlimited flexibility to add product attributes without schema changes, but that flexibility comes at a cost — every product load can spawn dozens of JOIN operations across multiple tables.&lt;/p&gt;

&lt;p&gt;If your category pages load slowly, your product detail pages feel sluggish, or your admin product grid takes forever to filter, EAV is almost certainly a contributing factor. This guide explains exactly how the EAV model works under the hood, why it struggles at scale, and what you can do about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Magento's EAV Model Works
&lt;/h2&gt;

&lt;p&gt;Unlike a traditional relational model where a "product" is a single row in a &lt;code&gt;products&lt;/code&gt; table, Magento stores product data across dozens of tables. The core EAV tables are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;catalog_product_entity&lt;/code&gt;&lt;/strong&gt; — the base entity table with only the entity ID, SKU, and a few fixed columns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;catalog_product_entity_{type}&lt;/code&gt;&lt;/strong&gt; — separate tables for each attribute type: &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;decimal&lt;/code&gt;, &lt;code&gt;varchar&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;datetime&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;eav_attribute&lt;/code&gt;&lt;/strong&gt; — the attribute registry defining all attributes and their metadata&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;catalog_product_entity_{type}_value&lt;/code&gt;&lt;/strong&gt; (legacy naming) — some versions use slightly different conventions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Loading a single product with 50 attributes typically requires 10–15 JOINs across these vertical tables. For a category page listing 20 products with their names, prices, and images, Magento can execute 50–100 individual queries — before you add any layered navigation or custom logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Magento Chose EAV
&lt;/h3&gt;

&lt;p&gt;Before you curse the architecture, understand why it exists. In a traditional e-commerce platform, adding a new product attribute requires an ALTER TABLE statement and a deployment. Magento merchants add hundreds of attributes through the admin panel without touching a line of SQL. The EAV model makes this possible by storing attributes as rows instead of columns.&lt;/p&gt;

&lt;p&gt;This is great for flexibility but terrible for read-heavy workloads — and e-commerce is about as read-heavy as it gets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Performance Cost of EAV
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Query Count Explosion
&lt;/h3&gt;

&lt;p&gt;The most visible symptom of EAV overhead is query count. On a default Magento 2 installation with 50 product attributes, loading a single product detail page generates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 query on &lt;code&gt;catalog_product_entity&lt;/code&gt; (the base row)&lt;/li&gt;
&lt;li&gt;~10 queries across the value tables (one per attribute group, depending on attribute types)&lt;/li&gt;
&lt;li&gt;1 query for the stock status&lt;/li&gt;
&lt;li&gt;~5 queries for category associations&lt;/li&gt;
&lt;li&gt;1 query for media gallery entries&lt;/li&gt;
&lt;li&gt;Additional queries for tier prices, special prices, and custom options&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's 20+ queries just to load one product. For a category page with 20 products and 4 visible attributes, Magento can execute 80+ queries in the catalog product list block alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  The JOIN Cost
&lt;/h3&gt;

&lt;p&gt;EAV queries rely heavily on LEFT JOINs. Here's what a typical product load query looks 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="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;at_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;AS&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;at_price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;at_status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;at_short_description&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;short_description&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_varchar&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;at_name&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;at_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;at_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;73&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;at_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_decimal&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;at_price&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;at_price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;at_price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;at_price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;at_status&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;at_status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;at_status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;84&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;at_status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_text&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;at_short_description&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;at_short_description&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;at_short_description&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;77&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;at_short_description&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now multiply this pattern across every product on the page and every attribute you query. With a catalog of 50,000 products and 100 attributes, your value tables have 5 million rows each. LEFT JOINs on those tables without proper indexing become expensive fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Store ID Factor
&lt;/h3&gt;

&lt;p&gt;Every EAV value query includes a &lt;code&gt;store_id&lt;/code&gt; condition. Magento queries for the default value first (&lt;code&gt;store_id = 0&lt;/code&gt;), then checks for store-scoped overrides. This doubles the query load — first for the default scope, then for the specific store view.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring Your EAV Performance
&lt;/h2&gt;

&lt;p&gt;Before optimizing, you need to measure the current state. These approaches help you identify EAV-related bottlenecks:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Enable the Built-In Profiler
&lt;/h3&gt;

&lt;p&gt;Magento's built-in profiler reveals the exact query count and execution time per request.&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;# Via env.php&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="nb"&gt;true&lt;/span&gt;,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via &lt;code&gt;app/etc/env.php&lt;/code&gt; set &lt;code&gt;xdebug&lt;/code&gt; mode in the profiler configuration. For production, redirect profiler output to a log file:&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/bootstrap.php&lt;/span&gt;
&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'MAGE_PROFILER'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'html'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// or 'csvfile' for production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Check the Slow Query Log
&lt;/h3&gt;

&lt;p&gt;EAV performance problems nearly always show up in MySQL's slow query log. Enable it and look for queries against &lt;code&gt;catalog_product_entity_*&lt;/code&gt; tables:&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;# my.cnf
&lt;/span&gt;&lt;span class="py"&gt;slow_query_log&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;slow_query_log_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/mysql/slow-eav.log&lt;/span&gt;
&lt;span class="py"&gt;long_query_time&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0.5&lt;/span&gt;
&lt;span class="py"&gt;log_queries_not_using_indexes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Queries running for 0.5+ seconds that touch EAV value tables are your optimization targets.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Monitor With New Relic or Blackfire
&lt;/h3&gt;

&lt;p&gt;Application performance monitoring tools show you exactly which EAV queries consume the most time. Look for &lt;code&gt;Magento\Eav\Model\Entity\AbstractEntity::loadAttributes()&lt;/code&gt; and &lt;code&gt;Magento\Eav\Model\Entity\AbstractEntity::getAttribute()&lt;/code&gt; in your traces.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  1. Reduce Attribute Count
&lt;/h3&gt;

&lt;p&gt;This is the simplest and most effective optimization. Every attribute you add to a product requires a JOIN in every product read operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit your attributes.&lt;/strong&gt; Disable or delete attributes that aren't used anywhere — not in listings, layered navigation, product pages, exports, or attribute sets. Magento ships with hundreds of attributes out of the box, and many stores use fewer than 30% of them.&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 attributes are actually used&lt;/span&gt;
bin/magento info:cron:list  &lt;span class="c"&gt;# Review attribute usage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better yet, use a SQL query to identify unused attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eav_attribute&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ea&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_varchar&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cpev&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cpev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_type_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;  &lt;span class="c1"&gt;-- product attributes&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;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="n"&gt;value_count&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attributes with zero values across your catalog are candidates for removal.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Move Computed Fields to Flat Tables
&lt;/h3&gt;

&lt;p&gt;For attributes that are read frequently but computed rarely (price rules applied to final prices, minimum advertised prices, etc.), consider storing computed values in a dedicated flat table instead of computing them via EAV joins.&lt;/p&gt;

&lt;p&gt;Magento's Flat Catalog feature does exactly this — it denormalizes EAV data into a single &lt;code&gt;catalog_product_flat&lt;/code&gt; table:&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;&lt;strong&gt;Important caveat:&lt;/strong&gt; Flat Catalog isn't a silver bullet. It adds index maintenance overhead and can cause issues with third-party extensions. Use it selectively — enable it for stores with 50+ attributes where attribute count is the bottleneck, and test thoroughly.&lt;/p&gt;

&lt;p&gt;For attribute sets with different profiles, flat tables use a "too many columns" strategy where unused columns are NULL. With many attribute sets, this can inflate table size dramatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Optimize EAV Table Indexing
&lt;/h3&gt;

&lt;p&gt;Magento's default indexing on EAV value tables is often insufficient for large catalogs. Ensure these indexes exist:&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;-- Critical composite index on value tables&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_varchar&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="nv"&gt;`EAV_ATTR_STORE_VALUE`&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_int&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="nv"&gt;`EAV_ATTR_STORE_VALUE`&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_decimal&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="nv"&gt;`EAV_ATTR_STORE_VALUE`&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_text&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="nv"&gt;`EAV_ATTR_STORE_VALUE`&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_datetime&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="nv"&gt;`EAV_ATTR_STORE_VALUE`&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default Magento indexes on these tables typically only cover &lt;code&gt;(entity_id)&lt;/code&gt; or &lt;code&gt;(entity_id, attribute_id)&lt;/code&gt;. Adding a composite index on &lt;code&gt;(attribute_id, store_id, entity_id)&lt;/code&gt; can improve attribute-level lookups by 5-10x.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Tweak EAV Batch Loading
&lt;/h3&gt;

&lt;p&gt;Magento's EAV model loads attributes in batches. By default, it loads all attributes for an entity type on first access. You can control this behavior:&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;!-- etc/eav_attributes.xml --&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:ObjectManager/etc/config.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&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\Eav\Model\Entity\Attribute\Loader"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;arguments&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;argument&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"batchSize"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;1000&lt;span class="nt"&gt;&amp;lt;/argument&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/arguments&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't reduce the total work, but it prevents memory exhaustion on large entity groups.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Use Direct Database Reads for Critical Paths
&lt;/h3&gt;

&lt;p&gt;For performance-critical code paths where you know exactly which attributes you need, bypass the EAV abstraction layer entirely:&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="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// Instead of: $product-&amp;gt;getName(), $product-&amp;gt;getPrice() ...&lt;/span&gt;
&lt;span class="c1"&gt;// Use a direct read for frequently accessed attributes:&lt;/span&gt;

&lt;span class="nv"&gt;$connection&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;resourceConnection&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="nv"&gt;$select&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;select&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;from&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'e'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'catalog_product_entity'&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;joinLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'at_name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'catalog_product_entity_varchar'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'e.entity_id = at_name.entity_id AND at_name.attribute_id = :name_attr AND at_name.store_id = :store_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'value'&lt;/span&gt;&lt;span class="p"&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;joinLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'at_price'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'catalog_product_entity_decimal'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'e.entity_id = at_price.entity_id AND at_price.attribute_id = :price_attr AND at_price.store_id = 0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'price'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'value'&lt;/span&gt;&lt;span class="p"&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;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'e.entity_id IN (?)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$entityIds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$bind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'name_attr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getNameAttributeId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'price_attr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getPriceAttributeId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'store_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$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="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$bind&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is especially useful for product collections in listings, sitemap generation, and export feeds where you need maximum throughput.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Cache Attribute Metadata
&lt;/h3&gt;

&lt;p&gt;EAV attribute metadata (attribute IDs, backend types, table names) is loaded on every request and cached in Magento's cache storage. Ensure your metadata cache is working:&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:enable eav
bin/magento cache:clean eav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the cache is populated:&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 KEYS &lt;span class="s1"&gt;'*EAV*'&lt;/span&gt;  &lt;span class="c"&gt;# If using Redis&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see cache misses on EAV metadata, check that your cache backend is properly configured and has enough capacity.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Consider Dedicated Search Indexes
&lt;/h3&gt;

&lt;p&gt;For layered navigation and product listing operations, EAV value tables are the wrong tool. This is exactly what Elasticsearch/OpenSearch excels at.&lt;/p&gt;

&lt;p&gt;When Magento's search engine is configured, filtered attribute values are read from the search index, not from EAV tables. Ensure your layered navigation attributes are set to "Use in Search" rather than "Use in Layered Navigation" with the "Filterable (no results)" option for best performance.&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;# Set all layered navigation attributes to use the search engine&lt;/span&gt;
bin/magento config:set catalog/search/engine elasticsearch7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This moves the query burden from MySQL EAV JOINs to the search engine's inverted indexes, which handle attribute filtering natively and efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study: A 60% Reduction in Product Page Load Time
&lt;/h2&gt;

&lt;p&gt;I recently worked with a Magento store running 2.4.6 with 45,000 products and 120+ product attributes. Their category pages loaded in 4.2 seconds and individual product pages took 2.8 seconds.&lt;/p&gt;

&lt;p&gt;Here's what we did:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Audited attributes:&lt;/strong&gt; Removed 28 unused attributes (23% reduction)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Added composite indexes:&lt;/strong&gt; On all five value tables with the &lt;code&gt;(attribute_id, store_id, entity_id)&lt;/code&gt; pattern&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enabled Flat Catalog:&lt;/strong&gt; For the main store view&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimized attribute loading:&lt;/strong&gt; For category listings, we switched to direct reads for the four attributes displayed on category pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ported layered navigation:&lt;/strong&gt; Moved filtering to Elasticsearch entirely&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;Category page: 4.2s → 1.1s (73% improvement)&lt;/li&gt;
&lt;li&gt;Product detail page: 2.8s → 0.9s (68% improvement)&lt;/li&gt;
&lt;li&gt;Query count per product page: 84 → 19&lt;/li&gt;
&lt;li&gt;Admin product grid: 8.3s → 1.7s (79% improvement)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not every store will see this level of improvement, but the pattern holds: EAV optimization consistently delivers 40-60% reductions in page load time for stores with 50+ attributes.&lt;/p&gt;

&lt;h2&gt;
  
  
  EAV Anti-Patterns to Avoid
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Loading All Attributes When You Need One
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// BAD: Loads the entire product with all attributes&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;productRepository&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sku&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;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// GOOD: Load only the attribute you need&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;productRepository&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// The 'true' parameter enables edit mode but disables attribute pre-loading&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Processing Products One at a Time
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// BAD: N+1 problem — loads each product individually&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;$skuList&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$sku&lt;/span&gt;&lt;span class="p"&gt;)&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;productRepository&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sku&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// process...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// GOOD: Use a collection with selected attributes&lt;/span&gt;
&lt;span class="nv"&gt;$collection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;productCollectionFactory&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;$collection&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;'sku'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$collection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addFieldToFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sku'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'in'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$skuList&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using EAV for Read-Heavy Flows
&lt;/h3&gt;

&lt;p&gt;The EAV model is optimized for write flexibility, not read speed. For any read-heavy code path — sitemaps, exports, API responses, search indexing — always batch your reads and select only the attributes you actually need.&lt;/p&gt;

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

&lt;p&gt;The EAV model is Magento's architectural compromise between flexibility and performance. It serves a critical purpose, but it's not free. Every attribute you add increases the tax on every product read operation.&lt;/p&gt;

&lt;p&gt;The most effective EAV performance strategy is a three-pronged approach: &lt;strong&gt;eliminate&lt;/strong&gt; unused attributes, &lt;strong&gt;index&lt;/strong&gt; the value tables properly, and &lt;strong&gt;bypass&lt;/strong&gt; the EAV abstraction layer for performance-critical paths with direct database reads.&lt;/p&gt;

&lt;p&gt;Start with an attribute audit — it costs nothing and often delivers the biggest gains. Add composite indexes on your value tables as a second step. Only then consider architectural changes like Flat Catalog or full search-engine migration for layered navigation.&lt;/p&gt;

&lt;p&gt;Your mileage will vary depending on catalog size, attribute count, and server hardware, but the principles are universal: fewer attributes, better indexes, and smarter query patterns will make your Magento store noticeably faster.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>performance</category>
      <category>eav</category>
      <category>mysql</category>
    </item>
    <item>
      <title>Magento 2 PHP-FPM Tuning: Process Manager, Pool Configuration &amp; Performance</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Tue, 02 Jun 2026 09:03:06 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-php-fpm-tuning-process-manager-pool-configuration-performance-26eg</link>
      <guid>https://dev.to/magevanta/magento-2-php-fpm-tuning-process-manager-pool-configuration-performance-26eg</guid>
      <description>&lt;p&gt;PHP-FPM is the engine room of every Magento 2 store. Get it wrong and your perfectly-optimised Nginx, Redis, and OPcache setup still falls over under load. Get it right and you'll handle traffic spikes cleanly without throwing 502 errors or burning CPU on context-switching between hundreds of idle workers.&lt;/p&gt;

&lt;p&gt;This guide covers everything from choosing the right process manager to calculating the optimal &lt;code&gt;pm.max_children&lt;/code&gt;, sizing memory limits, enabling the slow-log, and verifying your changes actually stick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PHP-FPM Configuration Matters for Magento
&lt;/h2&gt;

&lt;p&gt;Magento is memory-hungry. A single PHP worker handling a catalog page, a checkout step, or an API call typically consumes 80–200 MB of RAM. Multiply that by every concurrent request and you quickly exceed available memory—at which point Linux starts swapping, latency explodes, and your monitoring goes red.&lt;/p&gt;

&lt;p&gt;The PHP-FPM process manager sits between Nginx and PHP itself. It decides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many PHP processes run simultaneously&lt;/li&gt;
&lt;li&gt;Whether new processes are spawned on demand or kept alive in a warm pool&lt;/li&gt;
&lt;li&gt;How long an idle worker waits before being killed&lt;/li&gt;
&lt;li&gt;How requests are queued when all workers are busy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those decisions directly affects Magento's response time and resource usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the Right Process Manager (&lt;code&gt;pm&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;PHP-FPM ships with three process manager modes: &lt;code&gt;static&lt;/code&gt;, &lt;code&gt;dynamic&lt;/code&gt;, and &lt;code&gt;ondemand&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;pm = static&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;All workers are started at boot and kept alive forever. Memory is reserved upfront—no warm-up delay for the first request, no forking overhead under load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; dedicated servers with predictable, continuous traffic. The worker count is fixed, so you need enough RAM to sustain it always.&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;pm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;static&lt;/span&gt;
&lt;span class="py"&gt;pm.max_children&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;pm = dynamic&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Workers are started on demand up to &lt;code&gt;pm.max_children&lt;/code&gt;, but a minimum pool (&lt;code&gt;pm.min_spare_servers&lt;/code&gt;) is kept alive so sudden bursts don't start from zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Magento stores with variable traffic—daytime peak, overnight lull. This is the most common production setting.&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;pm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;dynamic&lt;/span&gt;
&lt;span class="py"&gt;pm.max_children&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;30&lt;/span&gt;
&lt;span class="py"&gt;pm.start_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;pm.min_spare_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;pm.max_spare_servers&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;pm.max_requests&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;500&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;pm = ondemand&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Workers are created for each request and killed after &lt;code&gt;pm.process_idle_timeout&lt;/code&gt; seconds of inactivity. Zero memory cost when idle, but cold-start latency on the first request after a quiet period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; dev environments, staging, or low-traffic stores where saving RAM matters more than latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculating &lt;code&gt;pm.max_children&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the most important setting. Too low → 502 errors under load. Too high → OOM kills and swap thrashing.&lt;/p&gt;

&lt;p&gt;The formula:&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;pm.max_children&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;(Available RAM) / (Average PHP worker RSS)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1 — Measure real Magento worker RSS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;--no-headers&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; rss,comm &lt;span class="nt"&gt;-C&lt;/span&gt; php-fpm | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{ sum += $1 } END { print sum/NR/1024 " MB avg" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a typical Magento 2 store with OPcache enabled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Storefront page: &lt;strong&gt;80–120 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Checkout / payment: &lt;strong&gt;120–160 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Admin panel: &lt;strong&gt;150–200 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;GraphQL / REST API: &lt;strong&gt;100–140 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the &lt;strong&gt;90th-percentile figure&lt;/strong&gt; from production, not the minimum. For most Magento stores, &lt;strong&gt;110–130 MB&lt;/strong&gt; is a safe baseline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Determine available RAM
&lt;/h3&gt;

&lt;p&gt;Available RAM = Total RAM − OS overhead − MySQL / Redis / Varnish / Nginx memory&lt;/p&gt;

&lt;p&gt;On a 16 GB server running only Nginx + PHP-FPM + Redis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;16384 MB
  -  512 MB  OS + system
  - 2048 MB  MySQL (innodb_buffer_pool_size + connections)
  -  512 MB  Redis
  -  128 MB  Nginx
= 13184 MB available for PHP-FPM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3 — Divide
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;13184 / 120 ≈ 109
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Round down with a 10–15 % safety margin: &lt;strong&gt;&lt;code&gt;pm.max_children = 90&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Leave headroom for memory spikes (large imports, complex cart rules, heavy admin operations).&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Production Pool Configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; /etc/php/8.3/fpm/pool.d/magento.conf
&lt;/span&gt;
&lt;span class="nn"&gt;[magento]&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;group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;www-data&lt;/span&gt;

&lt;span class="c"&gt;; Socket is faster than TCP for local Nginx
&lt;/span&gt;&lt;span class="py"&gt;listen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/run/php/php8.3-fpm-magento.sock&lt;/span&gt;
&lt;span class="py"&gt;listen.owner&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;listen.group&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;listen.mode&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0660&lt;/span&gt;

&lt;span class="c"&gt;; Process manager
&lt;/span&gt;&lt;span class="py"&gt;pm&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;dynamic&lt;/span&gt;
&lt;span class="py"&gt;pm.max_children&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;60&lt;/span&gt;
&lt;span class="py"&gt;pm.start_servers&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;pm.min_spare_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;pm.max_spare_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;20&lt;/span&gt;

&lt;span class="c"&gt;; Recycle workers after N requests to prevent memory leaks
&lt;/span&gt;&lt;span class="py"&gt;pm.max_requests&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;500&lt;/span&gt;

&lt;span class="c"&gt;; Kill a worker that has been processing for more than 60 s
&lt;/span&gt;&lt;span class="py"&gt;request_terminate_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;

&lt;span class="c"&gt;; Slow-log: capture stack traces for requests taking &amp;gt; 3 s
&lt;/span&gt;&lt;span class="py"&gt;slowlog&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/php-fpm/magento-slow.log&lt;/span&gt;
&lt;span class="py"&gt;request_slowlog_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;

&lt;span class="c"&gt;; PHP settings override per pool
&lt;/span&gt;&lt;span class="err"&gt;php_admin_value&lt;/span&gt;&lt;span class="nn"&gt;[memory_limit]&lt;/span&gt;       &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;768M&lt;/span&gt;
&lt;span class="err"&gt;php_admin_value&lt;/span&gt;&lt;span class="nn"&gt;[max_execution_time]&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;600&lt;/span&gt;
&lt;span class="err"&gt;php_admin_value&lt;/span&gt;&lt;span class="nn"&gt;[error_log]&lt;/span&gt;          &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;/var/log/php-fpm/magento-error.log&lt;/span&gt;
&lt;span class="err"&gt;php_flag&lt;/span&gt;&lt;span class="nn"&gt;[display_errors]&lt;/span&gt;            &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;off&lt;/span&gt;

&lt;span class="c"&gt;; Environment variables
&lt;/span&gt;&lt;span class="err"&gt;env&lt;/span&gt;&lt;span class="nn"&gt;[MAGE_MODE]&lt;/span&gt;     &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;production&lt;/span&gt;
&lt;span class="err"&gt;env&lt;/span&gt;&lt;span class="nn"&gt;[MAGE_RUN_TYPE]&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;store&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Memory Limit: &lt;code&gt;768M&lt;/code&gt; vs &lt;code&gt;2G&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;You'll see &lt;code&gt;memory_limit = 2G&lt;/code&gt; in many Magento "hardening" guides. That's a cargo-cult setting borrowed from import scripts. For storefront requests:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request type&lt;/th&gt;
&lt;th&gt;Realistic limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storefront page&lt;/td&gt;
&lt;td&gt;256M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout / order&lt;/td&gt;
&lt;td&gt;512M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin panel general&lt;/td&gt;
&lt;td&gt;512M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI / imports&lt;/td&gt;
&lt;td&gt;2G–4G (separate pool or CLI ini)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Set &lt;code&gt;memory_limit = 768M&lt;/code&gt; in your web pool and override with &lt;code&gt;php_admin_value[memory_limit] = 4G&lt;/code&gt; in a dedicated CLI pool or your &lt;code&gt;php.ini&lt;/code&gt; for &lt;code&gt;php-cli&lt;/code&gt;. This prevents a single rogue storefront worker from consuming 2 GB while leaving other workers starved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate Admin Pool (Optional but Recommended)
&lt;/h2&gt;

&lt;p&gt;Admin users run heavier operations—mass attribute updates, report generation, catalogue imports, cache flushes. Give them their own pool so they never crowd out storefront workers:&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/php/8.3/fpm/pool.d/magento-admin.conf
&lt;/span&gt;
&lt;span class="nn"&gt;[magento-admin]&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;group&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;listen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/run/php/php8.3-fpm-magento-admin.sock&lt;/span&gt;

&lt;span class="py"&gt;pm&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;dynamic&lt;/span&gt;
&lt;span class="py"&gt;pm.max_children&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;pm.start_servers&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;2&lt;/span&gt;
&lt;span class="py"&gt;pm.min_spare_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;pm.max_spare_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;4&lt;/span&gt;
&lt;span class="py"&gt;pm.max_requests&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;100&lt;/span&gt;

&lt;span class="err"&gt;php_admin_value&lt;/span&gt;&lt;span class="nn"&gt;[memory_limit]&lt;/span&gt;       &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;1G&lt;/span&gt;
&lt;span class="err"&gt;php_admin_value&lt;/span&gt;&lt;span class="nn"&gt;[max_execution_time]&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;900&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Nginx, route &lt;code&gt;/admin/&lt;/code&gt; and &lt;code&gt;/index.php/admin/&lt;/code&gt; to the admin socket and everything else to the storefront socket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;^/index\.php(/|$)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;$fpm_socket&lt;/span&gt; &lt;span class="n"&gt;/run/php/php8.3-fpm-magento.sock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request_uri&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;"^/admin")&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;$fpm_socket&lt;/span&gt; &lt;span class="n"&gt;/run/php/php8.3-fpm-magento-admin.sock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using the Slow-Log to Find Magento Bottlenecks
&lt;/h2&gt;

&lt;p&gt;The PHP-FPM slow-log captures a full PHP stack trace for every request that exceeds &lt;code&gt;request_slowlog_timeout&lt;/code&gt;. It is one of the fastest ways to spot Magento performance regressions without attaching a profiler.&lt;/p&gt;

&lt;p&gt;Enable it (see pool config above), reproduce the slow request, then inspect:&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;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/php-fpm/magento-slow.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sample output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[02-Jun-2026 09:14:32]  [pool magento] pid 12345
script_filename = /var/www/magento/index.php
[0x00007f...] catalogProductRepository-&amp;gt;get() /vendor/magento/module-catalog/Model/ProductRepository.php:208
[0x00007f...] \Magento\Catalog\Model\Layer\Category\ItemCollectionProvider-&amp;gt;getCollection() ...
[0x00007f...] \Magento\LayeredNavigation\Block\Navigation::_prepareLayout() ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The slow-log revealed layered navigation collection loading—a known hot path covered in our &lt;a href="https://dev.to/blog/magento-2-layered-navigation-performance"&gt;layered navigation performance guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring Pool Status
&lt;/h2&gt;

&lt;p&gt;PHP-FPM exposes a &lt;code&gt;/status&lt;/code&gt; endpoint showing active/idle workers, request queue length, and total requests served:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;^/fpm-status$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;allow&lt;/span&gt; &lt;span class="mf"&gt;127.0&lt;/span&gt;&lt;span class="s"&gt;.0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt;  &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_pass&lt;/span&gt; &lt;span class="s"&gt;unix:/run/php/php8.3-fpm-magento.sock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;fastcgi_params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_param&lt;/span&gt; &lt;span class="s"&gt;SCRIPT_FILENAME&lt;/span&gt; &lt;span class="nv"&gt;$document_root$fastcgi_script_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable in pool config:&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;pm.status_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/fpm-status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://127.0.0.1/fpm-status?full
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Warning threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;listen queue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt; 0 for sustained periods → increase &lt;code&gt;pm.max_children&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;active processes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Consistently at &lt;code&gt;pm.max_children&lt;/code&gt; → workers exhausted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max active processes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Near &lt;code&gt;pm.max_children&lt;/code&gt; → approaching limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;slow requests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Growing fast → investigate slow-log&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plug &lt;code&gt;/fpm-status&lt;/code&gt; into Prometheus (via &lt;code&gt;php-fpm_exporter&lt;/code&gt;), Datadog, or New Relic for time-series alerting.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;pm.max_requests&lt;/code&gt;: Preventing Memory Leaks
&lt;/h2&gt;

&lt;p&gt;Magento 2 and many third-party modules are not perfectly clean: objects accumulate in memory across requests, registry entries linger, and some custom code leaks closures. Setting &lt;code&gt;pm.max_requests = 500&lt;/code&gt; instructs PHP-FPM to gracefully restart each worker after 500 handled requests, clearing all accumulated memory.&lt;/p&gt;

&lt;p&gt;The trade-off is a tiny warm-up cost (OPcache re-fill for the new worker) which is negligible at &lt;code&gt;max_requests = 500&lt;/code&gt;. For stores with lots of third-party extensions or known leak issues, drop this to &lt;code&gt;200–300&lt;/code&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;[ ] Use &lt;code&gt;pm = dynamic&lt;/code&gt; for production Magento&lt;/li&gt;
&lt;li&gt;[ ] Calculate &lt;code&gt;pm.max_children&lt;/code&gt; from real RSS measurements, not guesses&lt;/li&gt;
&lt;li&gt;[ ] Keep &lt;code&gt;memory_limit&lt;/code&gt; ≤ 768M for web pools; use a separate high-memory pool for CLI&lt;/li&gt;
&lt;li&gt;[ ] Enable &lt;code&gt;request_terminate_timeout = 60s&lt;/code&gt; to kill runaway workers&lt;/li&gt;
&lt;li&gt;[ ] Set &lt;code&gt;pm.max_requests = 500&lt;/code&gt; to periodically recycle workers&lt;/li&gt;
&lt;li&gt;[ ] Enable the slow-log in staging/production for regression detection&lt;/li&gt;
&lt;li&gt;[ ] Expose &lt;code&gt;/fpm-status&lt;/code&gt; internally and monitor queue length + active workers&lt;/li&gt;
&lt;li&gt;[ ] Create a separate admin pool with lower &lt;code&gt;pm.max_children&lt;/code&gt; and higher memory/time limits&lt;/li&gt;
&lt;li&gt;[ ] Prefer Unix sockets over TCP for Nginx ↔ PHP-FPM on the same host&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;After editing pool files, always validate before reloading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php-fpm8.3 &lt;span class="nt"&gt;-t&lt;/span&gt;             &lt;span class="c"&gt;# syntax check&lt;/span&gt;
systemctl reload php8.3-fpm

&lt;span class="c"&gt;# Confirm workers started&lt;/span&gt;
ps aux | &lt;span class="nb"&gt;grep &lt;/span&gt;php-fpm | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;

&lt;span class="c"&gt;# Watch status under a load test&lt;/span&gt;
watch &lt;span class="nt"&gt;-n1&lt;/span&gt; &lt;span class="s2"&gt;"curl -s http://127.0.0.1/fpm-status | grep -E 'active|idle|queue'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;ab&lt;/code&gt; (Apache Bench) or &lt;code&gt;wrk&lt;/code&gt; for a quick load test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wrk &lt;span class="nt"&gt;-t4&lt;/span&gt; &lt;span class="nt"&gt;-c50&lt;/span&gt; &lt;span class="nt"&gt;-d30s&lt;/span&gt; https://yourstore.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the FPM status during the test. If &lt;code&gt;listen queue&lt;/code&gt; climbs above 0, you need more workers (more RAM) or faster PHP execution (profiling, caching, fewer plugins).&lt;/p&gt;

&lt;p&gt;PHP-FPM tuning is not a one-and-done task. As your Magento store grows—more SKUs, more extensions, higher traffic—revisit &lt;code&gt;pm.max_children&lt;/code&gt; quarterly and after major upgrades. The fifteen minutes it takes to recalculate and re-tune pays for itself the first time you dodge a traffic-spike outage.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Magento 2 Third-Party Extension Performance Audit: Find and Fix Hidden Bottlenecks</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Mon, 01 Jun 2026 09:03:06 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-third-party-extension-performance-audit-find-and-fix-hidden-bottlenecks-35fg</link>
      <guid>https://dev.to/magevanta/magento-2-third-party-extension-performance-audit-find-and-fix-hidden-bottlenecks-35fg</guid>
      <description>&lt;p&gt;Third-party extensions are a double-edged sword. They add capabilities that would take months to build in-house, but every additional module adds plugins, observers, layout blocks, and database queries to every page load. A store with 40 extensions installed could easily have 200+ extra interceptors firing on a single request — and most developers have no idea which ones are costing them.&lt;/p&gt;

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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



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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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



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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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



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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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



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

&lt;/div&gt;



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

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



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

&lt;/div&gt;



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

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



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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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



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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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



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

&lt;/div&gt;



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

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

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

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

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

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

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



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

&lt;/div&gt;



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

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

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



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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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



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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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