<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Magevanta</title>
    <description>The latest articles on DEV Community by Magevanta (@magevanta).</description>
    <link>https://dev.to/magevanta</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3887629%2F7af145fd-03e9-4362-99dc-a5f637f09ce1.png</url>
      <title>DEV Community: Magevanta</title>
      <link>https://dev.to/magevanta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/magevanta"/>
    <language>en</language>
    <item>
      <title>Magento 2 Async Operations and Message Queues: A Performance Deep Dive</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Fri, 08 May 2026 07:42:57 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-async-operations-and-message-queues-a-performance-deep-dive-42no</link>
      <guid>https://dev.to/magevanta/magento-2-async-operations-and-message-queues-a-performance-deep-dive-42no</guid>
      <description>&lt;p&gt;Every Magento 2 store has operations that are too slow to run synchronously during a web request. Sending transactional emails. Updating inventory across thousands of products. Re-indexing after bulk catalog changes. Running price calculations for complex customer segments.&lt;/p&gt;

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

&lt;p&gt;Configuration is split across three XML files in your module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;etc/&lt;/span&gt;
&lt;span class="s"&gt;├── queue_topology.xml&lt;/span&gt;   &lt;span class="c1"&gt;# Defines exchanges and bindings&lt;/span&gt;
&lt;span class="s"&gt;├── queue_publisher.xml&lt;/span&gt;  &lt;span class="c1"&gt;# Connects topics to exchanges&lt;/span&gt;
&lt;span class="s"&gt;├── queue_consumer.xml&lt;/span&gt;   &lt;span class="c1"&gt;# Defines consumer handlers&lt;/span&gt;
&lt;span class="s"&gt;└── communication.xml&lt;/span&gt;    &lt;span class="c1"&gt;# Defines topic schemas&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;For anything beyond development, use RabbitMQ. MySQL queues work but have limitations under load — they use polling, table locks, and don't support priority queues or dead letter exchanges.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ubuntu/Debian&lt;/span&gt;
apt &lt;span class="nb"&gt;install &lt;/span&gt;rabbitmq-server
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;rabbitmq-server
systemctl start rabbitmq-server

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Magento Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/etc/env.php&lt;/span&gt;
&lt;span class="s1"&gt;'queue'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'amqp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'host'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'5672'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'user'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'magento'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'your-password'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'virtualhost'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'ssl'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a dedicated RabbitMQ user:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



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



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

&lt;/div&gt;



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

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

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

&lt;h3&gt;
  
  
  Step 1: Define the Topic Schema
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- etc/communication.xml --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?xml version="1.0"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;config&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
        &lt;span class="na"&gt;xsi:noNamespaceSchemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"urn:magento:framework:Communication/etc/communication.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;topic&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"mycompany.erp.product.sync"&lt;/span&gt; &lt;span class="na"&gt;request=&lt;/span&gt;&lt;span class="s"&gt;"MyCompany\ErpSync\Api\Data\SyncRequestInterface"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Configure the Publisher
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- etc/queue_publisher.xml --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?xml version="1.0"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;config&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
        &lt;span class="na"&gt;xsi:noNamespaceSchemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"urn:magento:framework-message-queue:etc/publisher.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;publisher&lt;/span&gt; &lt;span class="na"&gt;topic=&lt;/span&gt;&lt;span class="s"&gt;"mycompany.erp.product.sync"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;connection&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"amqp"&lt;/span&gt; &lt;span class="na"&gt;exchange=&lt;/span&gt;&lt;span class="s"&gt;"magento"&lt;/span&gt; &lt;span class="na"&gt;disabled=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Fallback to MySQL if RabbitMQ unavailable --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;connection&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"db"&lt;/span&gt; &lt;span class="na"&gt;exchange=&lt;/span&gt;&lt;span class="s"&gt;"magento-db"&lt;/span&gt; &lt;span class="na"&gt;disabled=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/publisher&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Configure Topology (RabbitMQ)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- etc/queue_topology.xml --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?xml version="1.0"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;config&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
        &lt;span class="na"&gt;xsi:noNamespaceSchemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"urn:magento:framework-message-queue:etc/topology.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;exchange&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"magento"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"topic"&lt;/span&gt; &lt;span class="na"&gt;connection=&lt;/span&gt;&lt;span class="s"&gt;"amqp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;binding&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"erpSyncBinding"&lt;/span&gt;
                 &lt;span class="na"&gt;topic=&lt;/span&gt;&lt;span class="s"&gt;"mycompany.erp.product.sync"&lt;/span&gt;
                 &lt;span class="na"&gt;destinationType=&lt;/span&gt;&lt;span class="s"&gt;"queue"&lt;/span&gt;
                 &lt;span class="na"&gt;destination=&lt;/span&gt;&lt;span class="s"&gt;"mycompany.erp.product.sync"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/exchange&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Define the Consumer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- etc/queue_consumer.xml --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?xml version="1.0"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;config&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
        &lt;span class="na"&gt;xsi:noNamespaceSchemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"urn:magento:framework-message-queue:etc/consumer.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;consumer&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"myCompanyErpProductSync"&lt;/span&gt;
              &lt;span class="na"&gt;queue=&lt;/span&gt;&lt;span class="s"&gt;"mycompany.erp.product.sync"&lt;/span&gt;
              &lt;span class="na"&gt;connection=&lt;/span&gt;&lt;span class="s"&gt;"amqp"&lt;/span&gt;
              &lt;span class="na"&gt;consumerInstance=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Framework\MessageQueue\Consumer"&lt;/span&gt;
              &lt;span class="na"&gt;handler=&lt;/span&gt;&lt;span class="s"&gt;"MyCompany\ErpSync\Model\ProductSyncHandler::processMessage"&lt;/span&gt;
              &lt;span class="na"&gt;maxMessages=&lt;/span&gt;&lt;span class="s"&gt;"1000"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Implement the Handler
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Model/ProductSyncHandler.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyCompany\ErpSync\Model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;MyCompany\ErpSync\Api\Data\SyncRequestInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Psr\Log\LoggerInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductSyncHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;ErpApiClient&lt;/span&gt; &lt;span class="nv"&gt;$erpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;processMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;SyncRequestInterface&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;erpClient&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;syncProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSku&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getProductData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Log but don't re-throw — Magento will handle dead letters&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ERP sync failed: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'sku'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSku&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6: Publish from Your Event Observer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Observer/ProductSaveAfter.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyCompany\ErpSync\Observer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Magento\Framework\MessageQueue\PublisherInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;MyCompany\ErpSync\Api\Data\SyncRequestInterfaceFactory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductSaveAfter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;\Magento\Framework\Event\ObserverInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;PublisherInterface&lt;/span&gt; &lt;span class="nv"&gt;$publisher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;SyncRequestInterfaceFactory&lt;/span&gt; &lt;span class="nv"&gt;$requestFactory&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Magento\Framework\Event\Observer&lt;/span&gt; &lt;span class="nv"&gt;$observer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$observer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getEvent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getProduct&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;requestFactory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setSku&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSku&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setProductData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;extractProductData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Non-blocking: returns immediately, consumer picks up async&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mycompany.erp.product.sync'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The admin product save now returns instantly. The ERP sync happens in the background.&lt;/p&gt;

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

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

&lt;h3&gt;
  
  
  Supervisor Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; /etc/supervisor/conf.d/magento-consumers.conf
&lt;/span&gt;&lt;span class="nn"&gt;[program:magento-erp-sync]&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/php /var/www/html/bin/magento queue:consumers:start myCompanyErpProductSync --max-messages=10000&lt;/span&gt;
&lt;span class="py"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/www/html&lt;/span&gt;
&lt;span class="py"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;www-data&lt;/span&gt;
&lt;span class="py"&gt;autostart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;autorestart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;startretries&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="py"&gt;stderr_logfile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/log/supervisor/magento-erp-sync.err.log&lt;/span&gt;
&lt;span class="py"&gt;stdout_logfile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/log/supervisor/magento-erp-sync.out.log&lt;/span&gt;
&lt;span class="py"&gt;stopasgroup&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;killasgroup&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[program:magento-inventory-reservations]&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/php /var/www/html/bin/magento queue:consumers:start inventoryQtyCounter --max-messages=10000&lt;/span&gt;
&lt;span class="py"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/www/html&lt;/span&gt;
&lt;span class="py"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;www-data&lt;/span&gt;
&lt;span class="py"&gt;autostart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;autorestart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;/div&gt;



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



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

&lt;/div&gt;



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

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

&lt;p&gt;For high-throughput scenarios, run multiple consumer instances in parallel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; Multiple parallel consumers for high-volume queue
&lt;/span&gt;&lt;span class="nn"&gt;[program:magento-erp-sync]&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/php /var/www/html/bin/magento queue:consumers:start myCompanyErpProductSync --max-messages=5000&lt;/span&gt;
&lt;span class="py"&gt;numprocs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;4               ; Run 4 parallel consumers&lt;/span&gt;
&lt;span class="py"&gt;process_name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%(program_name)s-%(process_num)02d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;For mass operations (importing thousands of products, bulk price updates, etc.), Magento's Async Bulk REST API is built on top of the message queue framework:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Async bulk product update&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://yourstore.com/rest/async/bulk/V1/products &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'[
    {"product": {"sku": "SKU001", "price": 29.99}},
    {"product": {"sku": "SKU002", "price": 39.99}},
    ...
  ]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;h3&gt;
  
  
  CLI Monitoring
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List all queues and their message counts&lt;/span&gt;
rabbitmqctl list_queues name messages consumers

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

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

&lt;/div&gt;



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

&lt;p&gt;Set up monitoring to alert when queue depth exceeds thresholds:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;p&gt;Message queues are optimized for small messages. Avoid putting large data blobs (full product objects, images) in messages. Instead, pass identifiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad: serialize the entire product&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'topic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// Could be 50KB+&lt;/span&gt;

&lt;span class="c1"&gt;// Good: pass just the ID&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'topic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'product_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt; &lt;span class="c1"&gt;// Bytes&lt;/span&gt;
&lt;span class="c1"&gt;// Consumer loads the product from DB when processing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Async?&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sending transactional emails&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;No customer-facing impact from delay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Syncing to external ERP/PIM&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;External API latency shouldn't block save&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulk product/price updates&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Can take minutes; use Async Bulk API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Re-indexing after bulk import&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Resource-intensive, not time-critical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updating cart totals&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;Customer expects immediate feedback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inventory check at checkout&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;Must be synchronous for accuracy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generating invoices&lt;/td&gt;
&lt;td&gt;⚠️ Depends&lt;/td&gt;
&lt;td&gt;Async OK if email delivery is the only output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;Magento 2's message queue framework is one of the platform's most underutilized performance tools. Most stores that struggle with slow admin saves, timeout-prone bulk operations, or overloaded PHP workers could benefit significantly from pushing work off the critical path.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;The fastest way to see what's being rendered is to enable Magento's built-in template hints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento dev:template-hints:enable &lt;span class="nt"&gt;--type&lt;/span&gt; frontend
bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;Enable Magento's built-in profiler for deeper insight:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento dev:profiler:enable html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Disable the profiler when done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento dev:profiler:disable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;For a non-visual audit, you can dump the full layout structure from the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# See all layout handles for a given page type&lt;/span&gt;
bin/magento dev:layout:deps &lt;span class="nt"&gt;--theme&lt;/span&gt; Magento/luma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or write a quick script to dump the merged layout XML for a specific handle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$layout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$objectManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Magento\Framework\View\LayoutInterface&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$layout&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'default'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'catalog_product_view'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$layout&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;asString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

&lt;p&gt;Look for patterns like this in &lt;code&gt;vendor/&lt;/code&gt; or &lt;code&gt;app/code/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- vendor/somevendor/somemodule/view/frontend/layout/default.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;referenceContainer&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"after.body.start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;block&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"SomeVendor\SomeModule\Block\Tracker"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"somevendor.tracker"&lt;/span&gt; &lt;span class="na"&gt;template=&lt;/span&gt;&lt;span class="s"&gt;"tracker.phtml"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/referenceContainer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

&lt;p&gt;Check how many handles are loaded on a typical page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$handles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$layout&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getHandles&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$handles&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// Ideally &amp;lt; 20 on a typical page&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;Magento widgets are powerful but expensive. Each widget instance triggers a database query to load its configuration and an additional layout merge. Stores with dozens of active widgets accumulate significant per-request overhead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check your widget count&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;instance_type&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;widget_instance&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;instance_type&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;Many themes and extensions use &lt;code&gt;&amp;lt;remove name="block.name"/&amp;gt;&lt;/code&gt; to hide blocks. This is wasteful — the block is still instantiated before being removed. The correct approach is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Wrong: instantiates block then removes it --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;remove&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"breadcrumbs"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Better: prevent rendering via remove attribute --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;referenceBlock&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"breadcrumbs"&lt;/span&gt; &lt;span class="na"&gt;remove=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;remove="true"&lt;/code&gt; attribute on &lt;code&gt;&amp;lt;referenceBlock&amp;gt;&lt;/code&gt; prevents rendering without instantiating unnecessarily. The cleanest approach is to override the parent layout and simply not include the block in the first place.&lt;/p&gt;

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

&lt;p&gt;Blocks that execute expensive logic in &lt;code&gt;_prepareLayout()&lt;/code&gt; run during layout building, before any output is generated. This is particularly bad because &lt;code&gt;_prepareLayout()&lt;/code&gt; runs even if the block's output is cached.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad: runs on every page even if block output is cached&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;_prepareLayout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;productCollection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;collectionFactory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addAttributeToSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'*'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setPageSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Full collection load in _prepareLayout!&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;_prepareLayout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Better: lazy load in getter&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getProducts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;productCollection&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;productCollection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;collectionFactory&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addAttributeToSelect&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'price'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'url_key'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setPageSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;productCollection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Block Caching: Your Most Powerful Tool
&lt;/h2&gt;

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

&lt;h3&gt;
  
  
  Adding Cache to a Block
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyBlock&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;\Magento\Framework\View\Element\Template&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getCacheLifetime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1 hour&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getCacheKeyInfo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'MY_BLOCK'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;_storeManager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;_design&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getDesignTheme&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;httpContext&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Magento\Customer\Model\Context&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CONTEXT_GROUP&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;getCacheKeyInfo()&lt;/code&gt; method determines cache uniqueness. Include everything that affects the block's output — store ID, theme, customer group — but nothing more.&lt;/p&gt;

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

&lt;p&gt;For blocks that need to be personalized per customer but are otherwise static, consider Edge Side Includes (ESI) with Varnish. The main page is fully cached; the personalized block is fetched separately via a fast ESI request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- In your block's template --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;esi:include&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;?= $block-&amp;gt;getEsiUrl() ?&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially effective for the minicart, customer welcome message, and recently viewed products — blocks that prevent FPC from caching an otherwise static page.&lt;/p&gt;

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

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

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

&lt;p&gt;Magento's default layout includes many blocks that most stores don't need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- In your theme's default.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;page&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Remove if you don't use compare functionality --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;referenceBlock&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"catalog.compare.sidebar"&lt;/span&gt; &lt;span class="na"&gt;remove=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Remove if you use a custom cookie notice --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;referenceBlock&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"cookie-status-message"&lt;/span&gt; &lt;span class="na"&gt;remove=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Remove newsletter block if handled differently --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;referenceBlock&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"form.subscribe"&lt;/span&gt; &lt;span class="na"&gt;remove=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/page&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Defer Below-the-Fold Blocks
&lt;/h3&gt;

&lt;p&gt;Blocks that appear below the fold (product tabs, related products, upsells) don't need to render synchronously. Consider using lazy-loading via JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Replace block with a container that JS will populate --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;referenceBlock&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"product.info.upsell"&lt;/span&gt; &lt;span class="na"&gt;remove=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Add a lightweight placeholder instead --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;referenceContainer&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"product.info.main"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;block&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Framework\View\Element\Template"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"upsell.lazy.placeholder"&lt;/span&gt;
           &lt;span class="na"&gt;template=&lt;/span&gt;&lt;span class="s"&gt;"Your_Module::upsell-lazy.phtml"&lt;/span&gt; &lt;span class="na"&gt;after=&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/referenceContainer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;Magento caches the merged layout XML in the cache backend. If this cache is being invalidated too frequently, you'll pay the merge cost repeatedly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check layout cache keys (Redis backend)&lt;/span&gt;
redis-cli &lt;span class="nt"&gt;-n&lt;/span&gt; 0 keys &lt;span class="s2"&gt;"LAYOUT_*"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Causes of frequent layout cache invalidation:&lt;/p&gt;

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

&lt;p&gt;Ensure your deployment process flushes layout cache cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento cache:flush layout block_html full_page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

&lt;p&gt;Use these commands before and after optimization to quantify improvements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# TTFB measurement via curl (bypasses FPC with cache-busting header)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"TTFB: %{time_starttransfer}s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Cache-Control: no-cache"&lt;/span&gt; https://yourstore.com/your-category

&lt;span class="c"&gt;# Run 5 times and average&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;1..5&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{time_starttransfer}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://yourstore.com/your-category
&lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{sum+=$1} END {print "Average TTFB:", sum/NR, "seconds"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;More importantly, ensure FPC is enabled and warm for the rest of your store, because a fast product → cart → checkout funnel depends on each step being optimized.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify FPC is enabled&lt;/span&gt;
bin/magento cache:status | &lt;span class="nb"&gt;grep &lt;/span&gt;full_page

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

&lt;/div&gt;



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

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

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

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

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

&lt;p&gt;For production, use the built-in bundling or switch to a more advanced bundler:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



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

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

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

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

&lt;p&gt;Move non-critical third-party scripts (analytics, chat widgets, social pixels) to load after the checkout has initialized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- layout/checkout_index_index.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;page&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;block&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"your.analytics"&lt;/span&gt; &lt;span class="na"&gt;template=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="na"&gt;after=&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="c"&gt;&amp;lt;!-- Load after checkout init --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/block&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/page&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Reduce AJAX Calls During Checkout
&lt;/h2&gt;

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

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

&lt;p&gt;Magento's "sections" system (customer data, cart, etc.) makes AJAX calls to &lt;code&gt;/customer/section/load/&lt;/code&gt; on various triggers. Audit which sections are being reloaded unnecessarily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check which sections are defined&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"sections"&lt;/span&gt; vendor/magento/module-customer/etc/frontend/sections.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove or merge section dependencies from custom modules that trigger unnecessary invalidations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Your module's etc/frontend/sections.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;config&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;action&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"your/controller/action"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"cart"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Only include sections you actually modify --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/action&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Preload Shipping Rates
&lt;/h3&gt;

&lt;p&gt;If your store has a predictable default shipping address (e.g., country-level), pre-populate the shipping estimator with defaults to avoid an extra round trip when the customer first opens checkout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Plugin on ShippingInformationManagement&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;afterSaveAddressInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$cartId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$addressInformation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Cache shipping rates per region/country for faster subsequent loads&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

&lt;p&gt;Ensure these tables are regularly optimized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check table fragmentation&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_free&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_length&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your_db'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'quote'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'quote_item'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'quote_address'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'quote_address_rate'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Optimize fragmented tables&lt;/span&gt;
&lt;span class="n"&gt;OPTIMIZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quote_item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quote_address&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Old quotes bloat these tables and slow down queries. Set up a cron to purge them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento cron:run &lt;span class="nt"&gt;--group&lt;/span&gt; index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;Some Magento installations are missing useful composite indexes on quote-related tables. Add them via a data patch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;moduleDataSetup&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getConnection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;moduleDataSetup&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'quote'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;moduleDataSetup&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getIdxName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'quote'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'is_active'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'is_active'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Payment Method Optimization
&lt;/h2&gt;

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

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

&lt;p&gt;Every payment provider adds its own JS SDK to the checkout. Audit what's loading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check which payment methods are active&lt;/span&gt;
bin/magento config:show payment | &lt;span class="nb"&gt;grep &lt;/span&gt;active
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

&lt;p&gt;For PayPal specifically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Disable PayPal in-context checkout if not needed --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;config&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;default&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;paypal&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;wpp_usuk&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;in_context&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/in_context&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/wpp_usuk&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/paypal&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/default&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Server-Side Configuration
&lt;/h2&gt;

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

&lt;p&gt;Checkout is session-heavy. Storing sessions in files (the default) creates I/O contention under load. Move sessions to Redis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/etc/env.php&lt;/span&gt;
&lt;span class="s1"&gt;'session'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'save'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'redis'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'host'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6379'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2.5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'persistent_identifier'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'compression_threshold'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2048'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'compression_library'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'gzip'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'log_level'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'max_concurrency'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'break_after_frontend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'break_after_adminhtml'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'30'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'first_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'600'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'bot_first_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'60'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'bot_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'7200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'disable_locking'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'min_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'60'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'max_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2592000'&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable HTTP/2
&lt;/h3&gt;

&lt;p&gt;HTTP/2 multiplexing significantly reduces the overhead of multiple concurrent AJAX requests during checkout. Ensure your Nginx/Apache configuration has HTTP/2 enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;Each of these can be enabled or disabled independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento cache:status
bin/magento cache:enable full_page
bin/magento cache:disable block_html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;p&gt;If you're still on MySQL 5.7, keep the query cache disabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/mysql/mysql.conf.d/mysqld.cnf
&lt;/span&gt;&lt;span class="py"&gt;query_cache_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;query_cache_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The write-invalidation behavior combined with Magento's write-heavy workload makes it a net negative in most cases. Spend the memory on &lt;code&gt;innodb_buffer_pool_size&lt;/code&gt; instead — that's where your MySQL performance gains are.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# For a server with 16GB RAM dedicated to MySQL
&lt;/span&gt;&lt;span class="py"&gt;innodb_buffer_pool_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10G&lt;/span&gt;
&lt;span class="py"&gt;innodb_buffer_pool_instances&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

&lt;p&gt;For any production store, use &lt;strong&gt;Redis&lt;/strong&gt; as the cache backend. File-based caching is fine for development but falls apart under load due to filesystem contention.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento setup:config:set &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cache-backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redis &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cache-backend-redis-server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;127.0.0.1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cache-backend-redis-port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6379 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cache-backend-redis-db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the session cache, use a separate Redis instance or at least a separate database index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento setup:config:set &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-save&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redis &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-save-redis-host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;127.0.0.1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-save-redis-port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6379 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-save-redis-db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;In &lt;code&gt;app/etc/env.php&lt;/code&gt;, confirm FPC is set to Redis (or Varnish if applicable):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'cache'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'frontend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'full_page'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'backend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Cm_Cache_Backend_Redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'backend_options'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'server'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6379'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For high-traffic stores, put Varnish in front:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento config:set &lt;span class="nt"&gt;--scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;default system/full_page_cache/caching_application 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;Run &lt;code&gt;SHOW PROCESSLIST&lt;/code&gt; during peak traffic on an unoptimized store and you'll typically see a flood of queries like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`sales_order`&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;`customer_id`&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; 
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nv"&gt;`created_at`&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; 
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nv"&gt;`status`&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

&lt;p&gt;Check your current setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento config:show customer/magento_customersegment/is_enabled
bin/magento config:show customer/magento_customersegment/real_time_check_if_customer_is_logged_in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

&lt;p&gt;Add to &lt;code&gt;app/etc/env.php&lt;/code&gt; temporarily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'db'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'connection'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'profiler'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'class'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'\\Magento\\Framework\\DB\\Profiler'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'enabled'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then analyze your logs or use a tool like New Relic, Blackfire, or even &lt;code&gt;EXPLAIN&lt;/code&gt; on suspected queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Count Active Segments
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;magento_customersegment_segment&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More than 15–20 active segments? You're likely feeling the pain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check Segment Complexity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;segment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conditions_serialized&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;magento_customersegment_segment&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Segments with deeply nested conditions or those checking order history are the most expensive. Segments checking only customer attributes (email domain, group) are cheap.&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Or via CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento config:set customer/magento_customersegment/real_time_check_if_customer_is_logged_in 0
bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

&lt;p&gt;Disable unused segments immediately. Every active segment you remove eliminates queries per page load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check which segments are attached to price rules&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rule_name&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;magento_customersegment_segment&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;magento_customersegment_rule&lt;/span&gt; &lt;span class="n"&gt;cr&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;segment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;segment_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Segments with no attached rules are dead weight. Disable or delete them.&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;If segments query order or quote data, make sure the relevant columns are indexed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check for missing indexes on columns used in segment conditions&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

&lt;p&gt;Check your cron group configuration and consider moving segment processing to off-peak hours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Custom cron schedule example --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;group&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"customer_segment"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;schedule_generate_every&amp;gt;&lt;/span&gt;15&lt;span class="nt"&gt;&amp;lt;/schedule_generate_every&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;schedule_ahead_for&amp;gt;&lt;/span&gt;20&lt;span class="nt"&gt;&amp;lt;/schedule_ahead_for&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;schedule_lifetime&amp;gt;&lt;/span&gt;15&lt;span class="nt"&gt;&amp;lt;/schedule_lifetime&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;history_cleanup_every&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/history_cleanup_every&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;history_success_lifetime&amp;gt;&lt;/span&gt;60&lt;span class="nt"&gt;&amp;lt;/history_success_lifetime&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;history_failure_lifetime&amp;gt;&lt;/span&gt;600&lt;span class="nt"&gt;&amp;lt;/history_failure_lifetime&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;use_separate_process&amp;gt;&lt;/span&gt;1&lt;span class="nt"&gt;&amp;lt;/use_separate_process&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/group&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;For stores with custom segment conditions, consider caching the result in Redis with a TTL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer_segments_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$customerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$segments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;segmentHelper&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCustomerSegmentIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$customer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$segments&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;\Magento\Customer\Model\Customer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CACHE_TAG&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="c1"&gt;// 1 hour TTL&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is custom code but can be a lifesaver on high-traffic stores where even scheduled evaluation is too slow.&lt;/p&gt;

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

&lt;p&gt;After applying these optimizations, benchmark the impact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Use siege or ab for load testing&lt;/span&gt;
siege &lt;span class="nt"&gt;-c&lt;/span&gt; 10 &lt;span class="nt"&gt;-t&lt;/span&gt; 30S https://yourstore.com/customer/account/

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Magento 2 Database Connection Pooling: Reduce Overhead and Scale Faster</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Fri, 01 May 2026 09:02:04 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-database-connection-pooling-reduce-overhead-and-scale-faster-c36</link>
      <guid>https://dev.to/magevanta/magento-2-database-connection-pooling-reduce-overhead-and-scale-faster-c36</guid>
      <description>&lt;p&gt;Every Magento 2 page request that hits MySQL must first establish a database connection. On a busy store this sounds innocent, but at scale it becomes one of the biggest hidden bottlenecks: thousands of short-lived connections opening and closing per minute, each consuming CPU time, memory on the MySQL server, and precious milliseconds on the client side.&lt;/p&gt;

&lt;p&gt;Database connection pooling solves this by keeping a set of connections open and reusing them across requests. In this post we'll cover exactly what that means for Magento 2, how PHP's connection handling works under the hood, and the practical steps you can take today to reduce connection overhead and improve throughput.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Connection Overhead Matters
&lt;/h2&gt;

&lt;p&gt;When PHP opens a new MySQL connection it performs a TCP handshake, authenticates credentials, negotiates character sets, and allocates resources on both sides. On a modern server this takes roughly &lt;strong&gt;1–5 ms&lt;/strong&gt; per connection. That may sound trivial, but consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Magento page that fires 20 queries needs only &lt;strong&gt;one&lt;/strong&gt; connection — yet without pooling it creates and destroys that connection on every single request.&lt;/li&gt;
&lt;li&gt;Under a flash sale with 500 concurrent users, you could have &lt;strong&gt;500 simultaneous connection attempts&lt;/strong&gt; hitting MySQL at the same instant.&lt;/li&gt;
&lt;li&gt;MySQL's default &lt;code&gt;max_connections&lt;/code&gt; is 151. Exceed it and new requests receive &lt;code&gt;Too many connections&lt;/code&gt; errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: degraded performance exactly when you need it most.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Magento 2 Handles Connections
&lt;/h2&gt;

&lt;p&gt;Magento 2 uses its own &lt;code&gt;Magento\Framework\DB\Adapter\Pdo\Mysql&lt;/code&gt; which wraps PHP's PDO extension. By default, PDO opens a &lt;strong&gt;new connection per request&lt;/strong&gt; and closes it when PHP's process ends. There is no built-in connection pool at the application level.&lt;/p&gt;

&lt;p&gt;However, there are three layers where you can introduce pooling:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;PHP-FPM persistent connections&lt;/strong&gt; — simple, built into PHP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ProxySQL&lt;/strong&gt; — a dedicated MySQL proxy with true connection pooling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL connection management&lt;/strong&gt; via &lt;code&gt;wait_timeout&lt;/code&gt; and pool sizing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's look at each in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: PHP Persistent Connections
&lt;/h2&gt;

&lt;p&gt;PHP's PDO supports persistent connections via the &lt;code&gt;PDO::ATTR_PERSISTENT&lt;/code&gt; attribute. When enabled, PHP-FPM reuses an existing connection from the worker process instead of opening a new one.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to enable it in Magento 2
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;app/etc/env.php&lt;/code&gt; and add &lt;code&gt;persistent&lt;/code&gt; to your database configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'db'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'table_prefix'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'connection'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'host'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'dbname'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'magento'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'username'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'magento'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'secret'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'model'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'mysql4'&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;'innodb'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'initStatements'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'SET NAMES utf8;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'active'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'persistent'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;-- add this&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The trade-offs
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;Zero infrastructure change required&lt;/li&gt;
&lt;li&gt;Connection overhead drops significantly for long-running PHP-FPM workers&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Each PHP-FPM worker holds &lt;strong&gt;one persistent connection&lt;/strong&gt;. With 50 workers you have 50 permanent connections to MySQL.&lt;/li&gt;
&lt;li&gt;Transactions left open by a crashed worker can block subsequent requests on that same worker.&lt;/li&gt;
&lt;li&gt;Not suitable for multi-tenant setups or setups with frequent &lt;code&gt;SET&lt;/code&gt; statements that alter session state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Good for small-to-medium stores on a single server. Not recommended for large clusters without careful testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: ProxySQL — True Connection Pooling
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://proxysql.com/" rel="noopener noreferrer"&gt;ProxySQL&lt;/a&gt; is an open-source, high-performance MySQL proxy. It sits between Magento and MySQL and maintains a pool of backend connections that it multiplexes across incoming application connections.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Magento (PHP-FPM) → ProxySQL (:6033) → MySQL (:3306)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Magento connects to ProxySQL on port &lt;code&gt;6033&lt;/code&gt;. ProxySQL holds, say, 50 persistent connections to MySQL and distributes Magento's queries across them — even when 500 PHP workers are making requests simultaneously.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing ProxySQL on Ubuntu
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://github.com/sysown/proxysql/releases/latest/download/proxysql_2.7.1-ubuntu24_amd64.deb
dpkg &lt;span class="nt"&gt;-i&lt;/span&gt; proxysql_2.7.1-ubuntu24_amd64.deb
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;proxysql &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; systemctl start proxysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Basic ProxySQL configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Connect to ProxySQL admin&lt;/span&gt;
&lt;span class="n"&gt;mysql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;admin&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;padmin&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;P6032&lt;/span&gt;

&lt;span class="c1"&gt;-- Add your MySQL backend&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;mysql_servers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hostgroup_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&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="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3306&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Add Magento user&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;mysql_users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_hostgroup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'magento'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'secret'&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="c1"&gt;-- Configure connection pool size&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;global_variables&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;variable_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'50'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;variable_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'mysql-max_connections'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;global_variables&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;variable_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'12000'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;variable_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'mysql-wait_timeout'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;LOAD&lt;/span&gt; &lt;span class="n"&gt;MYSQL&lt;/span&gt; &lt;span class="n"&gt;SERVERS&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;RUNTIME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;SAVE&lt;/span&gt; &lt;span class="n"&gt;MYSQL&lt;/span&gt; &lt;span class="n"&gt;SERVERS&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;DISK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;LOAD&lt;/span&gt; &lt;span class="n"&gt;MYSQL&lt;/span&gt; &lt;span class="n"&gt;USERS&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;RUNTIME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="n"&gt;SAVE&lt;/span&gt; &lt;span class="n"&gt;MYSQL&lt;/span&gt; &lt;span class="n"&gt;USERS&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;DISK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;LOAD&lt;/span&gt; &lt;span class="n"&gt;MYSQL&lt;/span&gt; &lt;span class="n"&gt;VARIABLES&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;RUNTIME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;SAVE&lt;/span&gt; &lt;span class="n"&gt;MYSQL&lt;/span&gt; &lt;span class="n"&gt;VARIABLES&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;DISK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Point Magento at ProxySQL
&lt;/h3&gt;

&lt;p&gt;Update &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;'host'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6033'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// ProxySQL port&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What ProxySQL adds on top
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection multiplexing&lt;/strong&gt; — 500 PHP workers sharing 50 MySQL connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read/write split&lt;/strong&gt; — route &lt;code&gt;SELECT&lt;/code&gt; queries to replicas automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query routing rules&lt;/strong&gt; — send heavy analytical queries to a separate backend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query mirroring&lt;/strong&gt; — test new backend without affecting production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statistics&lt;/strong&gt; — per-query latency, error rates, connection pool stats&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Performance impact
&lt;/h3&gt;

&lt;p&gt;In benchmarks on a Magento 2 store with ~300 concurrent users:&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;Without ProxySQL&lt;/th&gt;
&lt;th&gt;With ProxySQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Avg response time&lt;/td&gt;
&lt;td&gt;420 ms&lt;/td&gt;
&lt;td&gt;310 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL &lt;code&gt;SHOW PROCESSLIST&lt;/code&gt; connections&lt;/td&gt;
&lt;td&gt;280+&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests/sec&lt;/td&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;td&gt;260&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Connection count dropped by 83% and throughput increased by 44%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: MySQL-Side Tuning
&lt;/h2&gt;

&lt;p&gt;Even without a proxy, proper MySQL configuration reduces the pain of many connections:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;wait_timeout&lt;/code&gt; and &lt;code&gt;interactive_timeout&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;By default MySQL keeps idle connections open for 8 hours (&lt;code&gt;wait_timeout = 28800&lt;/code&gt;). Lower this to reclaim resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/mysql/mysql.conf.d/mysqld.cnf
&lt;/span&gt;&lt;span class="py"&gt;wait_timeout&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;interactive_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;max_connections&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Size this based on your actual peak concurrency, not guesswork:&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;max_connections&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Too low: connection errors under load. Too high: MySQL allocates memory for each potential connection upfront, wasting RAM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thread pool plugin
&lt;/h3&gt;

&lt;p&gt;On MySQL Enterprise or Percona Server, enable the thread pool to limit simultaneous query execution even when many connections are open:&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;plugin-load-add&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;thread_pool.so&lt;/span&gt;
&lt;span class="py"&gt;thread_pool_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;16  # typically equal to CPU cores&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This dramatically reduces context-switching overhead under burst traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining the Layers
&lt;/h2&gt;

&lt;p&gt;The best production setup layers all three approaches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PHP-FPM (persistent=false) → ProxySQL (pool of 50) → MySQL (max_connections=100, thread_pool)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use persistent connections only if you &lt;strong&gt;cannot&lt;/strong&gt; deploy ProxySQL. If you have ProxySQL, disable PHP-level persistence — ProxySQL does it better and more safely.&lt;/p&gt;

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

&lt;p&gt;Always instrument after making changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  MySQL connection stats
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'Threads_connected'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'Connection_errors%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'Max_used_connections'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ProxySQL dashboard query
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;hostgroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;srv_host&lt;/span&gt;&lt;span class="p"&gt;,&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;ConnUsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ConnFree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ConnOK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ConnERR&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stats_mysql_connection_pool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  New Relic / Datadog
&lt;/h3&gt;

&lt;p&gt;If you use APM, watch the &lt;strong&gt;Database connection wait time&lt;/strong&gt; metric. A healthy store should spend &amp;lt; 1 ms waiting for a connection. Anything above 5 ms means your pool is undersized or MySQL is overloaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Recommendations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Store Size&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 50 req/s&lt;/td&gt;
&lt;td&gt;PHP persistent connections, tune &lt;code&gt;wait_timeout&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50–300 req/s&lt;/td&gt;
&lt;td&gt;ProxySQL with pool size 20–50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;300+ req/s&lt;/td&gt;
&lt;td&gt;ProxySQL + read replicas + thread pool&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start simple. Enable persistent connections first, benchmark with a tool like &lt;a href="https://k6.io/" rel="noopener noreferrer"&gt;k6&lt;/a&gt; or Apache Bench, then add ProxySQL if you still see connection contention.&lt;/p&gt;

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

&lt;p&gt;Database connection overhead is a silent killer of Magento 2 performance at scale. The fix isn't complicated, but it requires understanding the layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PHP persistent connections&lt;/strong&gt; give quick wins with no infrastructure changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ProxySQL&lt;/strong&gt; is the production-grade solution that truly pools and multiplexes connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL tuning&lt;/strong&gt; (&lt;code&gt;wait_timeout&lt;/code&gt;, &lt;code&gt;max_connections&lt;/code&gt;, thread pool) reduces server-side pressure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick the right level for your current scale, instrument properly, and you'll find that response times drop and MySQL stays calm even during your busiest sales days.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>mysql</category>
      <category>performance</category>
    </item>
    <item>
      <title>Magento 2 Logging Best Practices: Keep Your Logs Clean and Actionable</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Thu, 30 Apr 2026 09:02:13 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-logging-best-practices-keep-your-logs-clean-and-actionable-161h</link>
      <guid>https://dev.to/magevanta/magento-2-logging-best-practices-keep-your-logs-clean-and-actionable-161h</guid>
      <description>&lt;p&gt;Logs are your first line of defense when something goes wrong in production. But in a default Magento 2 installation, the logging setup is often either too noisy — filled with warnings that don't matter — or too quiet, missing the errors that actually need attention. Getting logging right is a force multiplier for your whole operations workflow.&lt;/p&gt;

&lt;p&gt;This guide covers how Magento 2 logging works under the hood, what you should configure, what to ignore, and how to build a lean, actionable logging setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Magento 2 Logging Works
&lt;/h2&gt;

&lt;p&gt;Magento uses &lt;a href="https://github.com/Seldaek/monolog" rel="noopener noreferrer"&gt;Monolog&lt;/a&gt; as its logging library. Log channels are defined as virtual types in &lt;code&gt;di.xml&lt;/code&gt;, and each module can register its own logger instance. By default, most logging goes to &lt;code&gt;var/log/system.log&lt;/code&gt; and &lt;code&gt;var/log/exception.log&lt;/code&gt;, but individual modules write to their own files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;var/log/system.log&lt;/code&gt; — general system messages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;var/log/exception.log&lt;/code&gt; — PHP exceptions caught by Magento&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;var/log/debug.log&lt;/code&gt; — debug-level messages (should be disabled in production)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;var/log/cron.log&lt;/code&gt; — cron job execution&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;var/log/payment.log&lt;/code&gt; — payment-related activity&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;var/log/shipping.log&lt;/code&gt; — shipping carrier requests and responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these can grow very large, very fast on a busy store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Disable Debug Logging in Production
&lt;/h2&gt;

&lt;p&gt;This one is critical. Magento's debug mode logs an enormous amount of data — request parameters, SQL queries, observer calls — and it should &lt;strong&gt;never&lt;/strong&gt; be enabled in production.&lt;/p&gt;

&lt;p&gt;Check your current setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php bin/magento config:show dev/debug/debug_logging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disable it:&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 config:set dev/debug/debug_logging 0
php bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also check &lt;code&gt;var/log/debug.log&lt;/code&gt; — if it exists and is growing, that's your culprit. On a high-traffic store this file can reach several GB within hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Implement Log Rotation
&lt;/h2&gt;

&lt;p&gt;Magento does not rotate logs automatically. Without rotation, log files will grow indefinitely until your disk fills up — and that will take your store down.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;logrotate&lt;/code&gt; on Linux. Create &lt;code&gt;/etc/logrotate.d/magento&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;/&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;www&lt;/span&gt;/&lt;span class="n"&gt;html&lt;/span&gt;/&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;log&lt;/span&gt;/*.&lt;span class="n"&gt;log&lt;/span&gt; {
    &lt;span class="n"&gt;daily&lt;/span&gt;
    &lt;span class="n"&gt;missingok&lt;/span&gt;
    &lt;span class="n"&gt;rotate&lt;/span&gt; &lt;span class="m"&gt;14&lt;/span&gt;
    &lt;span class="n"&gt;compress&lt;/span&gt;
    &lt;span class="n"&gt;delaycompress&lt;/span&gt;
    &lt;span class="n"&gt;notifempty&lt;/span&gt;
    &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="m"&gt;664&lt;/span&gt; &lt;span class="n"&gt;www&lt;/span&gt;-&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="n"&gt;www&lt;/span&gt;-&lt;span class="n"&gt;data&lt;/span&gt;
    &lt;span class="n"&gt;sharedscripts&lt;/span&gt;
    &lt;span class="n"&gt;postrotate&lt;/span&gt;
        /&lt;span class="n"&gt;usr&lt;/span&gt;/&lt;span class="n"&gt;bin&lt;/span&gt;/&lt;span class="n"&gt;find&lt;/span&gt; /&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;www&lt;/span&gt;/&lt;span class="n"&gt;html&lt;/span&gt;/&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;log&lt;/span&gt; -&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="s2"&gt;"*.log"&lt;/span&gt; -&lt;span class="n"&gt;mtime&lt;/span&gt; +&lt;span class="m"&gt;14&lt;/span&gt; -&lt;span class="n"&gt;delete&lt;/span&gt;
    &lt;span class="n"&gt;endscript&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adjust the path and user to match your setup. This keeps 14 days of compressed logs and removes anything older.&lt;/p&gt;

&lt;p&gt;You can also clean old logs via Magento's built-in cron task. Check &lt;code&gt;app/etc/crontab.xml&lt;/code&gt; — the &lt;code&gt;log_clean&lt;/code&gt; job runs and cleans database logs (customer log, quote log), but &lt;strong&gt;not&lt;/strong&gt; file-based logs in &lt;code&gt;var/log/&lt;/code&gt;. That's still your responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Fix Third-Party Module Log Spam
&lt;/h2&gt;

&lt;p&gt;A very common problem: a third-party module logs aggressively at WARNING or INFO level for things that aren't actually problems. These entries flood your logs and make it hard to spot real issues.&lt;/p&gt;

&lt;p&gt;To identify noisy modules, look at what's filling your &lt;code&gt;system.log&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'main\.\w+'&lt;/span&gt; var/log/system.log | &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;Or check by log level:&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;-c&lt;/span&gt; &lt;span class="s1"&gt;'WARNING'&lt;/span&gt; var/log/system.log
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'ERROR'&lt;/span&gt; var/log/system.log
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'CRITICAL'&lt;/span&gt; var/log/system.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If WARNING vastly outnumbers ERROR and CRITICAL, you have log spam. Track it to the source:&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="s1"&gt;'WARNING'&lt;/span&gt; var/log/system.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'in .*\.php'&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;p&gt;Once identified, you have two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Raise the minimum log level&lt;/strong&gt; for that module's handler in &lt;code&gt;di.xml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open a support ticket&lt;/strong&gt; with the extension vendor — this is their bug, not yours&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 4: Set Minimum Log Level Per Handler
&lt;/h2&gt;

&lt;p&gt;You can configure the minimum severity level for Magento's default log handlers via &lt;code&gt;di.xml&lt;/code&gt;. Add this to your module or &lt;code&gt;app/etc/di.xml&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="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Framework\Logger\Handler\System"&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;"fileName"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;/var/log/system.log&lt;span class="nt"&gt;&amp;lt;/argument&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;"loggerType"&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;400&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;loggerType&lt;/code&gt; corresponds to Monolog levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;100&lt;/code&gt; = DEBUG&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;200&lt;/code&gt; = INFO&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;250&lt;/code&gt; = NOTICE&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;300&lt;/code&gt; = WARNING&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;400&lt;/code&gt; = ERROR&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;500&lt;/code&gt; = CRITICAL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Setting &lt;code&gt;400&lt;/code&gt; (ERROR) means WARNING and below will be ignored. This is a reasonable production default for &lt;code&gt;system.log&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Write Better Custom Logs
&lt;/h2&gt;

&lt;p&gt;If you're developing a custom module, follow these guidelines to write logs that are actually useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the right level:&lt;/strong&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;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;'Processing item'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$itemId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;    &lt;span class="c1"&gt;// dev only&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;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Order exported'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'order'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$orderId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;  &lt;span class="c1"&gt;// informational&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;'Fallback used'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'reason'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$msg&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;   &lt;span class="c1"&gt;// worth noting&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Export failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'exception'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;    &lt;span class="c1"&gt;// needs attention&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;critical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Payment gateway down'&lt;/span&gt;&lt;span class="p"&gt;,&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="c1"&gt;// wake someone up&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Always include context:&lt;/strong&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;// Bad&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Failed to process order'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Good&lt;/span&gt;
&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Failed to process order'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'order_id'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;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;getIncrementId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'exception'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'trace'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTraceAsString&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;Context arrays are serialized to JSON in the log line, making them searchable and parseable by log aggregators.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a dedicated log channel&lt;/strong&gt; for your module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- di.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;virtualType&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"MyModule\Logger\Handler"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Framework\Logger\Handler\Base"&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;"fileName"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;/var/log/mymodule.log&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;/virtualType&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;virtualType&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"MyModule\Logger\Logger"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Framework\Logger\Monolog"&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;"name"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;mymodule&lt;span class="nt"&gt;&amp;lt;/argument&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;"handlers"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"array"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;item&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"system"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"object"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;MyModule\Logger\Handler&lt;span class="nt"&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
        &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;/virtualType&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps your logs isolated from Magento core logs and makes debugging much faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Monitor Critical Logs
&lt;/h2&gt;

&lt;p&gt;Logging is only useful if someone reads the logs. In production, set up alerting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simple option:&lt;/strong&gt; Use a cron job that runs &lt;code&gt;grep -c CRITICAL var/log/system.log&lt;/code&gt; and sends an alert if the count increases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better option:&lt;/strong&gt; Ship logs to a centralized service like &lt;strong&gt;Papertrail&lt;/strong&gt;, &lt;strong&gt;Logtail&lt;/strong&gt;, &lt;strong&gt;Datadog&lt;/strong&gt;, or &lt;strong&gt;Elasticsearch + Kibana&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best option:&lt;/strong&gt; Use structured JSON logging so your log aggregator can parse fields properly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For JSON logging in Magento, you can replace the default formatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Framework\Logger\Handler\System"&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;"formatter"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"object"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Monolog\Formatter\JsonFormatter&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON logs are harder to read manually but much easier to query, filter, and alert on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Don't Log Sensitive Data
&lt;/h2&gt;

&lt;p&gt;This sounds obvious but it happens — especially with payment modules. Never log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Credit card numbers or CVV codes&lt;/li&gt;
&lt;li&gt;Full customer passwords or tokens&lt;/li&gt;
&lt;li&gt;API keys or secrets&lt;/li&gt;
&lt;li&gt;Full HTTP request bodies from payment endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need to log payment-related activity for debugging, log only order IDs, masked card data (last 4 digits), and response codes. Many PCI DSS compliance failures originate in log files.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;dev/debug/debug_logging&lt;/code&gt; = 0 in production&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;logrotate&lt;/code&gt; configured for &lt;code&gt;var/log/*.log&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;var/log/debug.log&lt;/code&gt; doesn't exist or is empty&lt;/li&gt;
&lt;li&gt;[ ] System log filtered to ERROR level minimum&lt;/li&gt;
&lt;li&gt;[ ] Third-party log spam identified and addressed&lt;/li&gt;
&lt;li&gt;[ ] Critical log monitoring / alerting in place&lt;/li&gt;
&lt;li&gt;[ ] No sensitive data in any log file&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Good logging discipline makes the difference between a 5-minute incident response and a 5-hour debugging session. On Magento 2, the defaults are not production-ready — you need to actively configure log levels, implement rotation, suppress noise, and make sure critical errors reach the right people.&lt;/p&gt;

&lt;p&gt;Invest 30 minutes into your logging setup now and you'll save hours when something inevitably goes wrong.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>devops</category>
      <category>performance</category>
    </item>
    <item>
      <title>Magento 2 Multistore Performance: Architecture &amp; Optimization Guide</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Wed, 29 Apr 2026 09:04:54 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-multistore-performance-architecture-optimization-guide-lbj</link>
      <guid>https://dev.to/magevanta/magento-2-multistore-performance-architecture-optimization-guide-lbj</guid>
      <description>&lt;p&gt;Running multiple stores, websites, or store views on a single Magento 2 instance is one of the platform's biggest strengths. One codebase, one admin, one database — shared infrastructure for multiple brands, languages, or regions.&lt;/p&gt;

&lt;p&gt;But multistore setups come with hidden performance traps. The same features that make them flexible — scoped configuration, separate price rules, store-specific catalog data — can silently murder your response times if you're not careful.&lt;/p&gt;

&lt;p&gt;This guide covers the architecture decisions and practical optimizations that keep a multistore Magento 2 installation running fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Magento 2 Resolves Store Context
&lt;/h2&gt;

&lt;p&gt;Before optimizing, you need to understand the performance cost of store resolution itself.&lt;/p&gt;

&lt;p&gt;Every request, Magento determines the current store via one of these mechanisms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Server Name&lt;/strong&gt; — the default and recommended approach (each store on its own domain/subdomain)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL prefix&lt;/strong&gt; — store code in the URL path (&lt;code&gt;/uk/&lt;/code&gt;, &lt;code&gt;/de/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cookie&lt;/strong&gt; — least performant, not recommended for production&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;MAGE_RUN_CODE&lt;/code&gt; and &lt;code&gt;MAGE_RUN_TYPE&lt;/code&gt; environment variables (set in Nginx or &lt;code&gt;.htaccess&lt;/code&gt;) tell Magento which store to bootstrap. Getting this wrong — or relying on runtime detection — adds unnecessary overhead to every single request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set it at the web server level, not in PHP:&lt;/strong&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="c1"&gt;# Nginx example: domain-based store routing&lt;/span&gt;
&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;store-uk.example.com&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;MAGE_RUN_CODE&lt;/span&gt; &lt;span class="s"&gt;uk_store&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;MAGE_RUN_TYPE&lt;/span&gt; &lt;span class="s"&gt;store&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;store-de.example.com&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;MAGE_RUN_CODE&lt;/span&gt; &lt;span class="s"&gt;de_store&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;MAGE_RUN_TYPE&lt;/span&gt; &lt;span class="s"&gt;store&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 avoids the &lt;code&gt;\Magento\Store\Model\StoreResolver&lt;/code&gt; doing expensive database lookups on every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache Segmentation: The Biggest Pitfall
&lt;/h2&gt;

&lt;p&gt;By default, Magento stores cache entries with a cache key that includes the store context. But if you're not careful about cache tags, pages from one store can bleed into another — or worse, the cache hit rate tanks because every store generates separate entries for largely identical content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full Page Cache (FPC) Strategy
&lt;/h3&gt;

&lt;p&gt;Each store view should have a clean FPC namespace. With Varnish, this means:&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_hash&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;hash_data&lt;/span&gt;&lt;span class="p"&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.http.host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;hash_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.http.host&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;hash_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;server.ip&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;lookup&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;req.http.host&lt;/code&gt; inclusion ensures store A's cached homepage doesn't serve as store B's cache. Without it, you'll get cross-store cache pollution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis Cache Separation
&lt;/h3&gt;

&lt;p&gt;If you're running Redis for caching (and you should be), use &lt;strong&gt;separate Redis databases&lt;/strong&gt; per logical cache type — but don't over-segment by store. Magento's cache keys already include store scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/etc/env.php&lt;/span&gt;
&lt;span class="s1"&gt;'cache'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'frontend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'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;'backend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Cm_Cache_Backend_Redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'backend_options'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'server'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'page_cache'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'backend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Cm_Cache_Backend_Redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'backend_options'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'server'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="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;Separating &lt;code&gt;default&lt;/code&gt; and &lt;code&gt;page_cache&lt;/code&gt; into different Redis databases prevents FPC flushes from wiping your config/block cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration Scoping &amp;amp; Database Queries
&lt;/h2&gt;

&lt;p&gt;Magento's EAV-based configuration system is scope-aware. Every &lt;code&gt;core_config_data&lt;/code&gt; value can exist at global, website, or store level — and Magento merges them at runtime.&lt;/p&gt;

&lt;p&gt;In a multistore setup, this merge process runs on &lt;strong&gt;every request&lt;/strong&gt; (unless cached). With 5 stores × hundreds of config keys, you're looking at significant overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Profile it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento dev:query-log:enable
&lt;span class="c"&gt;# Run a few pages, then inspect var/debug/db.log&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"core_config_data"&lt;/span&gt; var/debug/db.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;&lt;strong&gt;Mitigate it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep config values at the &lt;strong&gt;global scope&lt;/strong&gt; whenever possible. Only override at store level when genuinely necessary.&lt;/li&gt;
&lt;li&gt;Avoid creating store-specific config values "just in case" — every scope override adds to the merge cost.&lt;/li&gt;
&lt;li&gt;Make sure &lt;code&gt;config&lt;/code&gt; cache type is always enabled in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Indexer Load in Multistore Environments
&lt;/h2&gt;

&lt;p&gt;Indexers are one of the biggest hidden costs in multistore Magento. The catalog price indexer, in particular, generates a price row for every product × customer group × website combination.&lt;/p&gt;

&lt;p&gt;With 3 websites, 4 customer groups, and 50,000 products:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 × 4 × 50,000 = 600,000 rows in catalog_product_index_price
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that to a single-store setup: 4 × 50,000 = 200,000 rows. You've tripled the index size just by adding two more websites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategies to reduce indexer bloat:&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Limit Customer Groups
&lt;/h3&gt;

&lt;p&gt;Every extra customer group multiplies your price index. Audit and remove unused groups via:&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;cg&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;cg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_group_code&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="k"&gt;c&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;customers&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer_group&lt;/span&gt; &lt;span class="n"&gt;cg&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;customer_entity&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cg&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="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cg&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="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Groups with zero customers are candidates for removal.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use Shared Catalogs Carefully
&lt;/h3&gt;

&lt;p&gt;B2B shared catalogs create additional price index entries. If you don't need per-company pricing, a simpler tier price setup is more performant.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Schedule Indexers Wisely
&lt;/h3&gt;

&lt;p&gt;In multistore setups, indexer runs take longer. Use &lt;code&gt;Update by Schedule&lt;/code&gt; mode but stagger your cron windows so the price indexer doesn't fire simultaneously across all websites:&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/magento indexer:set-mode schedule catalog_product_price&lt;/span&gt;
&lt;span class="c"&gt;# Then customize mview cron schedule in a custom module&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Store-Specific vs. Shared Catalog Data
&lt;/h2&gt;

&lt;p&gt;One of the biggest architectural decisions in multistore Magento is how much catalog data you share vs. scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared (global scope) — fast:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product names, descriptions, prices (if not localized)&lt;/li&gt;
&lt;li&gt;Categories with same URL structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Store-scoped — slower:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Translated attribute values (name, description per store)&lt;/li&gt;
&lt;li&gt;Store-specific URL rewrites&lt;/li&gt;
&lt;li&gt;Custom prices per website&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every store-scoped attribute value multiplies your EAV table size. A product with 10 text attributes across 5 store views = 50 rows in &lt;code&gt;catalog_product_entity_varchar&lt;/code&gt; vs. 10 for a single-store setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical rule:&lt;/strong&gt; Only create store-scoped overrides when there's a real business requirement. "We might translate this later" is not a reason to scope everything to store level today.&lt;/p&gt;

&lt;h2&gt;
  
  
  URL Rewrite Management
&lt;/h2&gt;

&lt;p&gt;URL rewrites are a notorious performance bottleneck, and it gets worse with multiple stores. Magento generates a URL rewrite record for every product × store view × category path combination.&lt;/p&gt;

&lt;p&gt;Check your current rewrite count:&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;store_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="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;rewrites&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;url_rewrite&lt;/span&gt; 
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;store_id&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;rewrites&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see millions of rows, you have a problem. Common causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Categories nested too deeply&lt;/strong&gt; — URL paths include all parent categories, so moving a category regenerates thousands of rewrites&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unnecessary store views&lt;/strong&gt; — disabled store views still have URL rewrites generated for them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product-category rewrites enabled&lt;/strong&gt; — the most common culprit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disable product-category URL rewrites if you don't need them:&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 → Catalog → Search Engine Optimization
→ Use Categories Path for Product URLs → No
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single change can reduce your &lt;code&gt;url_rewrite&lt;/code&gt; table by 60-80% in large catalogs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session Storage for Multiple Stores
&lt;/h2&gt;

&lt;p&gt;With multiple stores (especially across domains), session handling needs extra attention.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;Redis for sessions&lt;/strong&gt; — never filesystem sessions at scale&lt;/li&gt;
&lt;li&gt;If stores are on different domains, you cannot share sessions via cookie — each domain needs its own session namespace&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;path&lt;/code&gt; and &lt;code&gt;domain&lt;/code&gt; in session config per store if needed
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/etc/env.php&lt;/span&gt;
&lt;span class="s1"&gt;'session'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'save'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'redis'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'host'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6379'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'disable_locking'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="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;code&gt;disable_locking =&amp;gt; 1&lt;/code&gt; is important for performance — default session locking serializes concurrent requests from the same user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring Multistore Performance
&lt;/h2&gt;

&lt;p&gt;Set up per-store performance baselines. A single slow store can be hard to detect in aggregate metrics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New Relic / Datadog:&lt;/strong&gt; tag transactions with store code. In Magento, you can do this via a plugin on &lt;code&gt;\Magento\Store\Model\StoreManagerInterface::getStore()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple approach — separate log files per store:&lt;/strong&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;access_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/store_uk_access.log&lt;/span&gt; &lt;span class="s"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then track TTFB per store with a log parser. A sudden increase in one store's TTFB while others stay flat tells you exactly where to dig.&lt;/p&gt;

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

&lt;p&gt;Before diving into deep architecture changes, confirm these basics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;MAGE_RUN_CODE&lt;/code&gt; / &lt;code&gt;MAGE_RUN_TYPE&lt;/code&gt; set at Nginx level, not PHP&lt;/li&gt;
&lt;li&gt;[ ] Separate Redis databases for &lt;code&gt;default&lt;/code&gt; and &lt;code&gt;page_cache&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] All cache types enabled: &lt;code&gt;bin/magento cache:status&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Product-category URL rewrites disabled if not needed&lt;/li&gt;
&lt;li&gt;[ ] Unused customer groups removed&lt;/li&gt;
&lt;li&gt;[ ] Indexers in "Update by Schedule" mode&lt;/li&gt;
&lt;li&gt;[ ] Config values at global scope where possible&lt;/li&gt;
&lt;li&gt;[ ] Session locking disabled in Redis session config&lt;/li&gt;
&lt;li&gt;[ ] Each domain resolves via server_name, not URL prefix&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Multistore Magento is not inherently slow — but it amplifies every architectural mistake. The same configuration bloat, indexer inefficiency, or cache misconfiguration that causes minor issues on a single store becomes a serious problem at 3× or 5× scale.&lt;/p&gt;

&lt;p&gt;The good news: most of the wins are configuration changes, not code. Set your store resolution at the web server level, keep your config scope lean, manage your URL rewrites, and keep an eye on indexer growth as your catalog scales.&lt;/p&gt;

&lt;p&gt;Get these right and a multistore Magento setup can serve millions of requests efficiently — no extra infrastructure required.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Flat Catalog: Performance Impact &amp; When to Enable It</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Tue, 28 Apr 2026 09:04:10 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-flat-catalog-performance-impact-when-to-enable-it-p7l</link>
      <guid>https://dev.to/magevanta/magento-2-flat-catalog-performance-impact-when-to-enable-it-p7l</guid>
      <description>&lt;p&gt;The Magento 2 flat catalog sits in &lt;strong&gt;Stores → Configuration → Catalog → Storefront&lt;/strong&gt; as two innocent toggles: &lt;em&gt;Use Flat Catalog Category&lt;/em&gt; and &lt;em&gt;Use Flat Catalog Product&lt;/em&gt;. Many store owners enable both and call it a day, assuming more flat = more fast. The reality is more nuanced — and getting it wrong can actively &lt;em&gt;slow down&lt;/em&gt; your store.&lt;/p&gt;

&lt;p&gt;This post explains what the flat catalog actually does, the specific conditions where it helps, and a practical checklist for deciding whether to enable it on your store.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Flat Catalog Actually Does
&lt;/h2&gt;

&lt;p&gt;Magento's default EAV (Entity–Attribute–Value) model stores product and category data across many tables: &lt;code&gt;catalog_product_entity&lt;/code&gt;, &lt;code&gt;catalog_product_entity_varchar&lt;/code&gt;, &lt;code&gt;catalog_product_entity_int&lt;/code&gt;, &lt;code&gt;catalog_product_entity_decimal&lt;/code&gt;, and so on. Loading a single product can require joining dozens of tables.&lt;/p&gt;

&lt;p&gt;The flat catalog &lt;strong&gt;denormalizes&lt;/strong&gt; all of that into a single table per store view:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;catalog_product_flat_1&lt;/code&gt; (for store view ID 1)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;catalog_category_flat_store_1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of 20+ joins, Magento reads one row from one table. Sounds great on paper.&lt;/p&gt;

&lt;p&gt;The catch: that flat table must be &lt;strong&gt;rebuilt every time product data changes&lt;/strong&gt;. And the rebuild is expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Flat Catalog Helps
&lt;/h2&gt;

&lt;p&gt;The flat catalog genuinely shines in a specific scenario:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Large catalogs with mostly static data, heavy frontend traffic, and infrequent updates.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;50,000+ active products&lt;/li&gt;
&lt;li&gt;Multiple store views (one flat table per store view = optimized per-locale queries)&lt;/li&gt;
&lt;li&gt;Product data that changes weekly, not hourly&lt;/li&gt;
&lt;li&gt;A frontend-heavy workload (no headless/API-first architecture)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…then flat catalog can noticeably reduce query time on category pages and product listing pages.&lt;/p&gt;

&lt;p&gt;Benchmarks on large catalogs (100k+ products) commonly show &lt;strong&gt;20–40% faster category page render times&lt;/strong&gt; when flat catalog is enabled and the index is fresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Flat Catalog Hurts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Frequent product updates
&lt;/h3&gt;

&lt;p&gt;Every time a product attribute changes — price update, stock level, description edit — Magento has to re-flatten that data. If you're running price updates every few minutes (dynamic pricing, external ERP sync), you're constantly invalidating and rebuilding the flat index.&lt;/p&gt;

&lt;p&gt;During a full reindex, Magento falls back to EAV queries. Worse, a partial index lock can cause frontend errors if &lt;code&gt;catalog_product_flat_1&lt;/code&gt; is being swapped.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Small to medium catalogs
&lt;/h3&gt;

&lt;p&gt;For stores with under 10,000 products, the flat catalog overhead — index maintenance, table size, reindex time — rarely justifies itself. Modern MySQL with proper indexing handles EAV joins for small catalogs without breaking a sweat. Redis + full-page cache masks most of the remaining overhead anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Headless / API-first stores
&lt;/h3&gt;

&lt;p&gt;If your frontend hits Magento via GraphQL or REST, the flat catalog is largely irrelevant. GraphQL resolvers in Magento 2 use the product collection layer which has its own query optimization. The flat catalog was designed for the PHP-rendered Luma frontend.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. B2B stores with shared catalogs
&lt;/h3&gt;

&lt;p&gt;Magento's shared catalog (B2B module) adds per-company pricing and catalog visibility. Shared catalog relies heavily on the EAV layer and custom price indexing. Enabling flat catalog alongside shared catalog can cause subtle data inconsistencies, particularly with tier prices and customer group prices.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost: Index Table Size
&lt;/h2&gt;

&lt;p&gt;Let's talk storage. A standard EAV product table for 100,000 products might be 800MB across all attribute tables. The flat table for the &lt;em&gt;same&lt;/em&gt; catalog, across &lt;em&gt;four&lt;/em&gt; store views, can easily reach &lt;strong&gt;2–4GB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This matters for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;InnoDB buffer pool efficiency (can your flat tables fit in RAM?)&lt;/li&gt;
&lt;li&gt;Backup duration and storage cost&lt;/li&gt;
&lt;li&gt;Replication lag on read replicas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check your current flat table sizes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;data_length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;index_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;size_mb&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'catalog_%flat%'&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;size_mb&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those tables are larger than your InnoDB buffer pool allows to cache, you're doing disk I/O on every category page load — slower than EAV with a warm query cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Measure the Impact
&lt;/h2&gt;

&lt;p&gt;Before making a decision, benchmark your actual store. Here's a simple before/after approach:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Capture baseline
&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;# Enable slow query log temporarily&lt;/span&gt;
SET GLOBAL slow_query_log &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ON'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
SET GLOBAL long_query_time &lt;span class="o"&gt;=&lt;/span&gt; 0.1&lt;span class="p"&gt;;&lt;/span&gt;
SET GLOBAL slow_query_log_file &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/var/log/mysql/slow-baseline.log'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load 10–20 representative category pages via &lt;code&gt;curl&lt;/code&gt; or a tool like &lt;code&gt;wrk&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;wrk &lt;span class="nt"&gt;-t4&lt;/span&gt; &lt;span class="nt"&gt;-c20&lt;/span&gt; &lt;span class="nt"&gt;-d30s&lt;/span&gt; https://yourstore.com/category-url.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Enable flat catalog
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stores → Configuration → Catalog → Storefront
→ Use Flat Catalog Category: Yes
→ Use Flat Catalog Product: Yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then 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 catalog_category_flat catalog_product_flat
bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Re-run your benchmark
&lt;/h3&gt;

&lt;p&gt;Compare slow query counts, average response times, and MySQL CPU usage. If you see a &amp;gt;15% improvement, flat catalog is worth it for your setup. If the numbers barely move, stick with EAV.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flat Catalog &amp;amp; Magento Indexers
&lt;/h2&gt;

&lt;p&gt;The flat catalog depends on two indexers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Indexer&lt;/th&gt;
&lt;th&gt;Mode recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalog_category_flat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalog_product_flat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Never run these in realtime mode&lt;/strong&gt; unless you enjoy frontend slowdowns during product saves. Schedule mode batches the updates and runs them via cron, keeping your frontend serving fresh-enough data without locking tables mid-request.&lt;/p&gt;

&lt;p&gt;Configure via:&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_flat catalog_product_flat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And make sure your Magento cron is healthy:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Recommended Configuration Matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Store type&lt;/th&gt;
&lt;th&gt;Category flat&lt;/th&gt;
&lt;th&gt;Product flat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small store (&amp;lt;10k products)&lt;/td&gt;
&lt;td&gt;❌ Disable&lt;/td&gt;
&lt;td&gt;❌ Disable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium store (10k–50k), Luma frontend&lt;/td&gt;
&lt;td&gt;✅ Enable&lt;/td&gt;
&lt;td&gt;⚠️ Test first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large store (50k+), static prices&lt;/td&gt;
&lt;td&gt;✅ Enable&lt;/td&gt;
&lt;td&gt;✅ Enable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Headless / GraphQL frontend&lt;/td&gt;
&lt;td&gt;❌ Disable&lt;/td&gt;
&lt;td&gt;❌ Disable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2B with shared catalog&lt;/td&gt;
&lt;td&gt;❌ Disable&lt;/td&gt;
&lt;td&gt;❌ Disable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-frequency price updates&lt;/td&gt;
&lt;td&gt;❌ Disable&lt;/td&gt;
&lt;td&gt;❌ Disable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Disabling Flat Catalog Safely
&lt;/h2&gt;

&lt;p&gt;If you're currently running flat catalog and want to test without it:&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;# Disable via CLI (faster than UI)&lt;/span&gt;
bin/magento config:set catalog/frontend/flat_catalog_category 0
bin/magento config:set catalog/frontend/flat_catalog_product 0
bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Magento will automatically fall back to EAV queries. You don't need to drop the flat tables immediately — they'll just sit unused until you decide to re-enable or clean them up:&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;-- Only after confirming flat is disabled and you don't need rollback&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;catalog_product_flat_1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;catalog_category_flat_store_1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Repeat for each store view ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Real Performance Lever: Full-Page Cache
&lt;/h2&gt;

&lt;p&gt;Here's the honest truth that many performance guides skip: &lt;strong&gt;with full-page cache (FPC) properly configured, the flat catalog vs. EAV difference becomes almost irrelevant for most stores.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;FPC (Varnish or Magento's built-in cache) caches the entire rendered HTML page. That means the first request after a cache miss triggers the EAV or flat query — and every subsequent request until the cache expires serves static HTML without hitting the database at all.&lt;/p&gt;

&lt;p&gt;The flat catalog matters most in two scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cache miss rate is high (logged-in customers, personalized content)&lt;/li&gt;
&lt;li&gt;Your cron-based FPC warming can't keep up with catalog changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before spending time tuning flat catalog, verify your FPC hit rate:&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 Varnish hit rate&lt;/span&gt;
varnishstat &lt;span class="nt"&gt;-1&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;cache_hit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A healthy store should have &amp;gt;90% FPC hit rate. If you're below that, fix your caching strategy first — it will have 10x the impact of any flat catalog configuration.&lt;/p&gt;

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

&lt;p&gt;The flat catalog is a legitimate performance tool with a narrow sweet spot. Enable it when you have a large, stable Luma-frontend catalog. Disable it (or skip it entirely) for headless setups, B2B stores, high-frequency update pipelines, and small catalogs where the overhead isn't justified.&lt;/p&gt;

&lt;p&gt;Most importantly: &lt;strong&gt;measure before and after&lt;/strong&gt;. Performance decisions made on assumptions rather than data are how stores end up with configurations that were optimal in 2015 and actively harmful today.&lt;/p&gt;

&lt;p&gt;The best Magento performance stack in 2026 is still: &lt;strong&gt;proper indexing + Redis for cache/session + Varnish FPC + a well-tuned InnoDB buffer pool&lt;/strong&gt;. The flat catalog is a complementary optimization, not a foundation.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>performance</category>
      <category>php</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 CDN Configuration: Faster Assets, Global Performance</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Mon, 27 Apr 2026 09:02:19 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-cdn-configuration-faster-assets-global-performance-42ca</link>
      <guid>https://dev.to/magevanta/magento-2-cdn-configuration-faster-assets-global-performance-42ca</guid>
      <description>&lt;p&gt;A CDN (Content Delivery Network) is one of the highest-leverage performance improvements you can make to a Magento 2 store. Instead of every visitor downloading images, CSS, and JavaScript from your origin server, those assets are served from an edge node geographically close to them. The result: dramatically lower latency, reduced origin load, and better Core Web Vitals scores.&lt;/p&gt;

&lt;p&gt;This guide walks through the full CDN setup for Magento 2 — from choosing a provider to configuring static content URLs, media, and cache invalidation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Magento 2 Needs a CDN
&lt;/h2&gt;

&lt;p&gt;A typical Magento 2 page request involves dozens of static asset fetches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CSS bundles (merged or per-module)&lt;/li&gt;
&lt;li&gt;JavaScript files (RequireJS modules, bundles)&lt;/li&gt;
&lt;li&gt;Product images and banners&lt;/li&gt;
&lt;li&gt;Fonts, icons, and SVGs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without a CDN, every one of those requests hits your origin server. For a store with international customers, a visitor in Sydney hitting a server in Frankfurt adds 200–300 ms of network latency &lt;em&gt;per asset&lt;/em&gt;. Multiply that by 40–60 assets and you're looking at seconds of avoidable delay.&lt;/p&gt;

&lt;p&gt;A CDN solves this by caching assets at edge nodes around the world after the first request. Subsequent requests from nearby visitors are served at local network speeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a CDN Provider
&lt;/h2&gt;

&lt;p&gt;Magento 2 works with any CDN that supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom origin pull (fetches from your server on cache miss)&lt;/li&gt;
&lt;li&gt;Custom hostname / CNAME support&lt;/li&gt;
&lt;li&gt;Cache-Control header respect&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Popular choices:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free tier, excellent DDoS protection, easy setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fastly&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Used by Adobe Commerce Cloud, very fast purge API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS CloudFront&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Great if already on AWS, flexible but more complex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BunnyCDN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cheap, fast, straightforward pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KeyCDN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pay-as-you-go, simple configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most self-hosted stores, Cloudflare or BunnyCDN offer the best price/performance ratio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Set Up a CDN Subdomain
&lt;/h2&gt;

&lt;p&gt;Create a dedicated subdomain that points to your CDN, 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;cdn.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your CDN provider, configure this hostname as a "pull zone" or "distribution" with your Magento origin as the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;Origin&lt;/span&gt;: &lt;span class="n"&gt;https&lt;/span&gt;://&lt;span class="n"&gt;yourdomain&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="n"&gt;CDN&lt;/span&gt; &lt;span class="n"&gt;hostname&lt;/span&gt;: &lt;span class="n"&gt;cdn&lt;/span&gt;.&lt;span class="n"&gt;yourdomain&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CDN will pull files from your origin on the first request and cache them at the edge for subsequent requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Configure Magento 2 to Use the CDN URL
&lt;/h2&gt;

&lt;p&gt;In the Magento Admin, navigate to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stores → Configuration → General → Web&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;Base URLs&lt;/strong&gt;, set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Base URL for Static View Files&lt;/strong&gt;: &lt;code&gt;https://cdn.yourdomain.com/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base URL for User Media Files&lt;/strong&gt;: &lt;code&gt;https://cdn.yourdomain.com/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save the config and flush the Magento cache:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;You can also set this via CLI, which is useful for automated deployments:&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 web/unsecure/base_static_url https://cdn.yourdomain.com/
bin/magento config:set web/unsecure/base_media_url https://cdn.yourdomain.com/
bin/magento config:set web/secure/base_static_url https://cdn.yourdomain.com/
bin/magento config:set web/secure/base_media_url https://cdn.yourdomain.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this change, all references to static and media files in Magento's HTML output will point to your CDN URL instead of the origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Verify Static Content Deployment
&lt;/h2&gt;

&lt;p&gt;Before pushing to production, make sure your static content is deployed and accessible:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then visit a frontend page and inspect the asset URLs in your browser's network tab. You should see requests going to &lt;code&gt;cdn.yourdomain.com&lt;/code&gt; for CSS, JS, and images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Configure Cache Headers on the Origin
&lt;/h2&gt;

&lt;p&gt;For the CDN to cache assets effectively, your origin server needs to send proper &lt;code&gt;Cache-Control&lt;/code&gt; headers. Magento's static assets are versioned by hash, so they can be cached aggressively.&lt;/p&gt;

&lt;p&gt;In Nginx, add this to 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;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;.(jpg|jpeg|png|gif|ico|webp|svg|css|js|woff|woff2|ttf|eot)&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;add_header&lt;/span&gt; &lt;span class="s"&gt;Vary&lt;/span&gt; &lt;span class="s"&gt;"Accept-Encoding"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For media files (product images), a shorter TTL is safer since they can change:&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="n"&gt;/media/&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;7d&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="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;
  
  
  Step 5 — CDN Cache Invalidation
&lt;/h2&gt;

&lt;p&gt;When you deploy new static content (new theme, module update), the CDN needs to serve fresh files. There are two approaches:&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A — Version-based URLs (Recommended)
&lt;/h3&gt;

&lt;p&gt;Magento uses content hashes in static asset URLs by default (e.g., &lt;code&gt;version1234567890/&lt;/code&gt;). When you redeploy static content, the hash changes, so the CDN automatically fetches the new files. No manual invalidation needed.&lt;/p&gt;

&lt;p&gt;Make sure this is enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento config:set dev/static/sign 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option B — Manual CDN Purge
&lt;/h3&gt;

&lt;p&gt;If you need to force a purge (e.g., after updating media images), most CDN providers offer an API or CLI:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache"&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;"Authorization: Bearer {token}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"purge_everything":true}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;BunnyCDN:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.bunny.net/pullzone/{zone_id}/purgeCache"&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;"AccessKey: {api_key}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Fastly (used in Adobe Commerce Cloud pipelines), Magento has built-in Fastly integration with automatic purge on cache flush.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — Handle Secure vs Non-Secure URLs
&lt;/h2&gt;

&lt;p&gt;If your store runs on HTTPS (it should), make sure your CDN subdomain also has a valid TLS certificate. Most CDN providers issue free certificates automatically for custom domains.&lt;/p&gt;

&lt;p&gt;Also ensure both secure and non-secure base URLs are set in Magento (even if you redirect all HTTP to HTTPS):&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 web/unsecure/base_static_url https://cdn.yourdomain.com/
bin/magento config:set web/secure/base_static_url https://cdn.yourdomain.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7 — Exclude Dynamic Requests from the CDN
&lt;/h2&gt;

&lt;p&gt;Your CDN should only cache static assets, not Magento's dynamic pages (cart, checkout, customer account). Configure your CDN to &lt;strong&gt;not cache&lt;/strong&gt; requests matching:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paths containing session cookies (&lt;code&gt;PHPSESSID&lt;/code&gt;, &lt;code&gt;mage-cache-sessid&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/checkout/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/customer/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/sales/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Any request with a &lt;code&gt;Set-Cookie&lt;/code&gt; response header&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Cloudflare, use a Page Rule:&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="na"&gt;URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yourdomain.com/checkout/*&lt;/span&gt;
&lt;span class="na"&gt;Cache Level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bypass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Fastly/VCL, configure &lt;code&gt;pass&lt;/code&gt; for these paths in the &lt;code&gt;vcl_recv&lt;/code&gt; subroutine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Impact — What to Expect
&lt;/h2&gt;

&lt;p&gt;After a proper CDN setup, typical improvements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before CDN&lt;/th&gt;
&lt;th&gt;After CDN&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TTFB for static assets&lt;/td&gt;
&lt;td&gt;80–300 ms&lt;/td&gt;
&lt;td&gt;5–30 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP (Largest Contentful Paint)&lt;/td&gt;
&lt;td&gt;3.5–6s&lt;/td&gt;
&lt;td&gt;1.5–2.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Origin bandwidth usage&lt;/td&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;−60–80%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server load during traffic spikes&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Significantly reduced&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The bandwidth reduction alone can meaningfully lower hosting costs, especially for image-heavy catalogs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mixed content warnings:&lt;/strong&gt; If your CDN URL uses HTTPS but Magento generates some HTTP URLs, browsers will block the mixed content. Always use HTTPS for both origin and CDN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong path structure:&lt;/strong&gt; The CDN pull zone must serve Magento's full path structure, including &lt;code&gt;/pub/&lt;/code&gt; if your webroot is configured to use it. Test a few asset URLs manually before going live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgot to flush config cache:&lt;/strong&gt; After changing base URLs in the admin, always run &lt;code&gt;bin/magento cache:flush&lt;/code&gt; — stale config cache will cause the old URLs to persist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDN caching HTML:&lt;/strong&gt; If you accidentally route your main domain through the CDN without bypassing dynamic requests, customers will see cached versions of each other's carts. Always double-check your bypass rules.&lt;/p&gt;

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

&lt;p&gt;A CDN is one of the few Magento performance improvements with near-zero downside. Setup takes an hour, the cache miss penalty is invisible to users, and the gains are immediate and measurable.&lt;/p&gt;

&lt;p&gt;The recommended flow for a new store:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick a CDN provider (Cloudflare for simplicity, BunnyCDN for cost efficiency)&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;cdn.yourdomain.com&lt;/code&gt; pull zone pointing to your origin&lt;/li&gt;
&lt;li&gt;Set the Magento base static/media URLs&lt;/li&gt;
&lt;li&gt;Configure aggressive cache headers in Nginx&lt;/li&gt;
&lt;li&gt;Enable signed static URLs for automatic versioning&lt;/li&gt;
&lt;li&gt;Test asset URLs on staging first, then deploy to production&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once it's running, you'll wonder why you didn't set it up on day one.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>performance</category>
      <category>cdn</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Magento 2 CI/CD with GitHub Actions: A Production-Ready Deploy Pipeline</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 26 Apr 2026 09:02:02 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-cicd-with-github-actions-a-production-ready-deploy-pipeline-3g20</link>
      <guid>https://dev.to/magevanta/magento-2-cicd-with-github-actions-a-production-ready-deploy-pipeline-3g20</guid>
      <description>&lt;p&gt;Deploying Magento 2 manually is a recipe for mistakes — missed &lt;code&gt;setup:upgrade&lt;/code&gt;, forgotten cache flushes, or a static content deploy that never ran. GitHub Actions lets you automate every step, enforce consistency, and ship with confidence. In this guide we build a production-ready CI/CD pipeline from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why GitHub Actions for Magento 2?
&lt;/h2&gt;

&lt;p&gt;Magento deployments have a lot of moving parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Composer install (with auth.json for Magento repo)&lt;/li&gt;
&lt;li&gt;Database schema + data migrations (&lt;code&gt;setup:upgrade&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Dependency injection compilation (&lt;code&gt;setup:di:compile&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Static content deploy (&lt;code&gt;setup:static-content:deploy&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Cache flush&lt;/li&gt;
&lt;li&gt;Potentially a maintenance window&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One missed step can take your store down. Automating it removes human error and makes every deployment reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repository Structure
&lt;/h2&gt;

&lt;p&gt;Before setting up the pipeline, your repo should follow this layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;magento-root/
├── app/
├── composer.json
├── composer.lock
├── .github/
│   └── workflows/
│       └── deploy.yml
└── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;auth.json&lt;/code&gt; out of version control — we'll inject it via secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up GitHub Secrets
&lt;/h2&gt;

&lt;p&gt;Go to your repo → &lt;strong&gt;Settings → Secrets and variables → Actions&lt;/strong&gt; and add:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SSH_PRIVATE_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Private key for your server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SSH_HOST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Server IP or hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SSH_USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deploy user (e.g. &lt;code&gt;magento&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MAGENTO_REPO_PUBLIC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Magento Marketplace public key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MAGENTO_REPO_PRIVATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Magento Marketplace private key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DEPLOY_PATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Absolute path on server (e.g. &lt;code&gt;/var/www/magento&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Workflow File
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;:&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="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Magento &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&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;Deploy to Production&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&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;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;Setup PHP&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shivammathur/setup-php@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;php-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8.3'&lt;/span&gt;
          &lt;span class="na"&gt;extensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bcmath, ctype, curl, dom, gd, intl, mbstring, openssl, pdo_mysql, simplexml, soap, xsl, zip&lt;/span&gt;
          &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer:v2&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;Configure Magento Composer auth&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;composer config -g http-basic.repo.magento.com \&lt;/span&gt;
            &lt;span class="s"&gt;${{ secrets.MAGENTO_REPO_PUBLIC }} \&lt;/span&gt;
            &lt;span class="s"&gt;${{ secrets.MAGENTO_REPO_PRIVATE }}&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;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;composer install --no-dev --optimize-autoloader --no-interaction&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;Setup SSH&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;webfactory/ssh-agent@v0.9.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ssh-private-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SSH_PRIVATE_KEY }}&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;Add server to known hosts&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;ssh-keyscan -H ${{ secrets.SSH_HOST }} &amp;gt;&amp;gt; ~/.ssh/known_hosts&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;Sync files to server&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;rsync -az --delete \&lt;/span&gt;
            &lt;span class="s"&gt;--exclude='.git' \&lt;/span&gt;
            &lt;span class="s"&gt;--exclude='.github' \&lt;/span&gt;
            &lt;span class="s"&gt;--exclude='var/' \&lt;/span&gt;
            &lt;span class="s"&gt;--exclude='pub/media/' \&lt;/span&gt;
            &lt;span class="s"&gt;--exclude='app/etc/env.php' \&lt;/span&gt;
            &lt;span class="s"&gt;./ ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ secrets.DEPLOY_PATH }}/&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 Magento deploy commands&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;ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} &amp;lt;&amp;lt; 'EOF'&lt;/span&gt;
            &lt;span class="s"&gt;set -e&lt;/span&gt;
            &lt;span class="s"&gt;cd ${{ secrets.DEPLOY_PATH }}&lt;/span&gt;

            &lt;span class="s"&gt;# Enable maintenance mode&lt;/span&gt;
            &lt;span class="s"&gt;php bin/magento maintenance:enable&lt;/span&gt;

            &lt;span class="s"&gt;# Run upgrades&lt;/span&gt;
            &lt;span class="s"&gt;php bin/magento setup:upgrade --keep-generated&lt;/span&gt;

            &lt;span class="s"&gt;# Compile DI&lt;/span&gt;
            &lt;span class="s"&gt;php bin/magento setup:di:compile&lt;/span&gt;

            &lt;span class="s"&gt;# Deploy static content (adjust locales as needed)&lt;/span&gt;
            &lt;span class="s"&gt;php bin/magento setup:static-content:deploy en_US nl_NL -f&lt;/span&gt;

            &lt;span class="s"&gt;# Flush cache&lt;/span&gt;
            &lt;span class="s"&gt;php bin/magento cache:flush&lt;/span&gt;

            &lt;span class="s"&gt;# Disable maintenance&lt;/span&gt;
            &lt;span class="s"&gt;php bin/magento maintenance:disable&lt;/span&gt;

            &lt;span class="s"&gt;echo "Deploy complete ✓"&lt;/span&gt;
          &lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Optimizing the Pipeline
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cache Composer Dependencies
&lt;/h3&gt;

&lt;p&gt;Composer installs can take several minutes. Use GitHub's cache action to speed things up:&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;Cache Composer packages&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&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;~/.composer/cache&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ runner.os }}-composer-&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this step &lt;strong&gt;before&lt;/strong&gt; &lt;code&gt;composer install&lt;/code&gt;. On cache hit, installs drop from ~4 minutes to under 30 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel Static Content Deploy
&lt;/h3&gt;

&lt;p&gt;For stores with multiple locales, parallelize the static content deploy:&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 setup:static-content:deploy en_US nl_NL de_DE fr_FR &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--jobs&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;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--jobs&lt;/code&gt; flag uses multiple CPU cores. On a 4-core server this roughly cuts deploy time in half.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skip Compilation When Unnecessary
&lt;/h3&gt;

&lt;p&gt;If your push only changes templates or CMS content, running &lt;code&gt;setup:di:compile&lt;/code&gt; wastes 3–5 minutes. Use path filters:&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="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;paths-ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app/design/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.md'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use a more granular approach with conditional steps:&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;Check if PHP changed&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;php_changed&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;git diff --name-only HEAD~1 HEAD | grep -qE '\.php$' &amp;amp;&amp;amp; echo "changed=true" &amp;gt;&amp;gt; $GITHUB_OUTPUT || echo "changed=false" &amp;gt;&amp;gt; $GITHUB_OUTPUT&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;Compile DI&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.php_changed.outputs.changed == 'true'&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;ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \&lt;/span&gt;
            &lt;span class="s"&gt;"cd ${{ secrets.DEPLOY_PATH }} &amp;amp;&amp;amp; php bin/magento setup:di:compile"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Zero-Downtime with Symlink Strategy
&lt;/h2&gt;

&lt;p&gt;The maintenance window approach works, but causes downtime. A symlink-based blue/green strategy eliminates it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/www/
├── releases/
│   ├── 20260426_110000/   ← new release
│   └── 20260420_090000/   ← previous
├── shared/
│   ├── env.php
│   ├── var/
│   └── pub/media/
└── current -&amp;gt; releases/20260426_110000  ← symlink, Nginx points here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update your deploy step to:&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;Deploy with symlink swap&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;ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} &amp;lt;&amp;lt; 'EOF'&lt;/span&gt;
            &lt;span class="s"&gt;set -e&lt;/span&gt;
            &lt;span class="s"&gt;RELEASE_DIR="${{ secrets.DEPLOY_PATH }}/releases/$(date +%Y%m%d_%H%M%S)"&lt;/span&gt;
            &lt;span class="s"&gt;SHARED_DIR="${{ secrets.DEPLOY_PATH }}/shared"&lt;/span&gt;
            &lt;span class="s"&gt;CURRENT="${{ secrets.DEPLOY_PATH }}/current"&lt;/span&gt;

            &lt;span class="s"&gt;mkdir -p $RELEASE_DIR&lt;/span&gt;

            &lt;span class="s"&gt;# Copy files (already rsynced to a staging dir in previous step)&lt;/span&gt;
            &lt;span class="s"&gt;rsync -a ${{ secrets.DEPLOY_PATH }}/staging/ $RELEASE_DIR/&lt;/span&gt;

            &lt;span class="s"&gt;# Link shared files&lt;/span&gt;
            &lt;span class="s"&gt;ln -sfn $SHARED_DIR/env.php $RELEASE_DIR/app/etc/env.php&lt;/span&gt;
            &lt;span class="s"&gt;ln -sfn $SHARED_DIR/var $RELEASE_DIR/var&lt;/span&gt;
            &lt;span class="s"&gt;ln -sfn $SHARED_DIR/pub/media $RELEASE_DIR/pub/media&lt;/span&gt;

            &lt;span class="s"&gt;cd $RELEASE_DIR&lt;/span&gt;
            &lt;span class="s"&gt;php bin/magento setup:upgrade --keep-generated&lt;/span&gt;
            &lt;span class="s"&gt;php bin/magento setup:di:compile&lt;/span&gt;
            &lt;span class="s"&gt;php bin/magento setup:static-content:deploy en_US -f&lt;/span&gt;
            &lt;span class="s"&gt;php bin/magento cache:flush&lt;/span&gt;

            &lt;span class="s"&gt;# Atomic swap — no downtime&lt;/span&gt;
            &lt;span class="s"&gt;ln -sfn $RELEASE_DIR $CURRENT&lt;/span&gt;

            &lt;span class="s"&gt;# Keep last 5 releases&lt;/span&gt;
            &lt;span class="s"&gt;ls -dt ${{ secrets.DEPLOY_PATH }}/releases/*/ | tail -n +6 | xargs rm -rf&lt;/span&gt;
          &lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nginx points to &lt;code&gt;/var/www/current&lt;/code&gt; and the symlink swap is atomic — visitors never see a broken state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollback
&lt;/h2&gt;

&lt;p&gt;With the releases directory, rollback is trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List releases&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /var/www/releases/

&lt;span class="c"&gt;# Roll back to previous&lt;/span&gt;
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sfn&lt;/span&gt; /var/www/releases/20260420_090000 /var/www/current
php bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a manual rollback workflow in &lt;code&gt;.github/workflows/rollback.yml&lt;/code&gt; with &lt;code&gt;workflow_dispatch&lt;/code&gt; trigger so you can trigger it from the GitHub UI with one click.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notifications
&lt;/h2&gt;

&lt;p&gt;Add a Slack or Telegram notification at the end of the job:&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;Notify on success&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;success()&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;curl -s -X POST https://api.telegram.org/bot${{ secrets.TG_BOT_TOKEN }}/sendMessage \&lt;/span&gt;
            &lt;span class="s"&gt;-d chat_id=${{ secrets.TG_CHAT_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;-d text="✅ Magento deploy succeeded on $(date)"&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;Notify on failure&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failure()&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;curl -s -X POST https://api.telegram.org/bot${{ secrets.TG_BOT_TOKEN }}/sendMessage \&lt;/span&gt;
            &lt;span class="s"&gt;-d chat_id=${{ secrets.TG_CHAT_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;-d text="❌ Magento deploy FAILED — check GitHub Actions"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Security Considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never commit &lt;code&gt;auth.json&lt;/code&gt;&lt;/strong&gt; or &lt;code&gt;app/etc/env.php&lt;/code&gt;. Add them to &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use a &lt;strong&gt;dedicated deploy user&lt;/strong&gt; on the server with minimal permissions — no sudo, only write access to the web root.&lt;/li&gt;
&lt;li&gt;Rotate SSH keys periodically and use GitHub's secret rotation reminders.&lt;/li&gt;
&lt;li&gt;Consider &lt;strong&gt;branch protection rules&lt;/strong&gt;: require PRs to merge into &lt;code&gt;main&lt;/code&gt;, so deploys always go through review.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Complete Pipeline at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Time (estimate)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Checkout + PHP setup&lt;/td&gt;
&lt;td&gt;~20s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composer install (cached)&lt;/td&gt;
&lt;td&gt;~30s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rsync to server&lt;/td&gt;
&lt;td&gt;~15s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setup:upgrade&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~30s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setup:di:compile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~90s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static content deploy&lt;/td&gt;
&lt;td&gt;~60s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache flush&lt;/td&gt;
&lt;td&gt;~5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~4 minutes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four minutes from &lt;code&gt;git push&lt;/code&gt; to live — with zero manual steps and full rollback capability.&lt;/p&gt;

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

&lt;p&gt;A solid Magento 2 CI/CD pipeline with GitHub Actions gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repeatability&lt;/strong&gt; — every deploy follows the same steps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt; — cached dependencies and parallel jobs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety&lt;/strong&gt; — atomic symlink swaps eliminate downtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; — notifications on every deploy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery&lt;/strong&gt; — instant rollback to any previous release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with the basic workflow, then layer in the symlink strategy and parallelization as your store grows. Your future self will thank you at 2 AM when a deploy goes sideways and rollback takes 10 seconds instead of 45 minutes.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>devops</category>
      <category>cicd</category>
      <category>php</category>
    </item>
    <item>
      <title>Magento 2 JavaScript Bundling &amp; RequireJS Optimization</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sat, 25 Apr 2026 09:02:04 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-javascript-bundling-requirejs-optimization-472e</link>
      <guid>https://dev.to/magevanta/magento-2-javascript-bundling-requirejs-optimization-472e</guid>
      <description>&lt;p&gt;JavaScript is one of the biggest culprits behind slow Magento 2 storefronts. A default installation can easily generate &lt;strong&gt;100+ individual JS file requests&lt;/strong&gt; per page. Each one adds latency, blocks rendering, and hammers your Time to First Byte (TTFB). In this guide, we'll walk through Magento's built-in bundling system, how RequireJS works under the hood, and the concrete steps you can take to dramatically reduce JS overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Magento 2 JS is Slow by Default
&lt;/h2&gt;

&lt;p&gt;Magento 2 uses &lt;strong&gt;RequireJS&lt;/strong&gt; (AMD module format) to load JavaScript dependencies on demand. This is a flexible system, but it comes with a significant downside in production: the browser has to make a waterfall of individual HTTP requests to resolve the dependency graph at runtime.&lt;/p&gt;

&lt;p&gt;On a typical category page you'll see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;requirejs/require.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;requirejs-config.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Dozens of individual &lt;code&gt;*.js&lt;/code&gt; modules loaded on demand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even with HTTP/2 multiplexing, this many round trips add up — especially on mobile or high-latency connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Enable Production Mode
&lt;/h2&gt;

&lt;p&gt;Before any JS optimisation makes sense, make sure you're running in &lt;strong&gt;production mode&lt;/strong&gt;. Development mode intentionally skips most optimizations.&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 production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production mode Magento automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Merges and minifies CSS&lt;/li&gt;
&lt;li&gt;Deploys static assets to &lt;code&gt;pub/static/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Enables RequireJS optimizer output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the full static content deploy after switching:&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; en_US nl_NL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Enable JavaScript Merging and Minification
&lt;/h2&gt;

&lt;p&gt;In the Admin under &lt;strong&gt;Stores → Configuration → Advanced → Developer → JavaScript Settings&lt;/strong&gt;, enable:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Merge JavaScript Files&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable JavaScript Bundling&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minify JavaScript Files&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Or via CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento config:set dev/js/merge_files 1
bin/magento config:set dev/js/enable_js_bundling 1
bin/magento config:set dev/js/minify_files 1
bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Note:&lt;/strong&gt; Magento's built-in bundling creates a single large bundle per page type (homepage, category, product, etc.). This reduces request count dramatically but can increase initial payload size. Measure the impact with Lighthouse before and after.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: Understand the RequireJS Build System
&lt;/h2&gt;

&lt;p&gt;Magento ships with an &lt;strong&gt;r.js optimizer&lt;/strong&gt; workflow. When you run &lt;code&gt;setup:static-content:deploy&lt;/code&gt;, Magento processes your &lt;code&gt;requirejs-config.js&lt;/code&gt; files and can produce optimised bundles.&lt;/p&gt;

&lt;p&gt;The key file is &lt;code&gt;requirejs-config.js&lt;/code&gt; — each module can contribute to it via &lt;code&gt;view/frontend/requirejs-config.js&lt;/code&gt;. Badly written third-party configs are often the source of JS bloat.&lt;/p&gt;

&lt;p&gt;Check for conflicts or redundant map entries:&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;# Inspect the merged requirejs config for a theme&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;pub/static/frontend/Magento/luma/en_US/requirejs-config.js | python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"map&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;paths&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;shim"&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A healthy store should have a focused, clean config. If you're seeing hundreds of entries, a third-party module is likely over-declaring dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Identify Your Heaviest JavaScript Files
&lt;/h2&gt;

&lt;p&gt;Before blindly bundling, know what you're dealing 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;# Find the largest JS files in your static deploy&lt;/span&gt;
find pub/static/frontend &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.js"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.min.js"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | xargs &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="se"&gt;\&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;Common heavy hitters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;jquery.js&lt;/code&gt; / &lt;code&gt;jquery-ui.js&lt;/code&gt; — often loaded even when not needed on a page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mage/validation.js&lt;/code&gt; — heavy, only needed on checkout/forms&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Magento_Catalog/js/product/view/&lt;/code&gt; bundle — large on PDPs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Use Advanced JS Bundling (Grunt-based)
&lt;/h2&gt;

&lt;p&gt;Magento's default bundling is coarse-grained. For finer control, use the official &lt;strong&gt;Advanced JS Bundling&lt;/strong&gt; approach with Grunt.&lt;/p&gt;

&lt;p&gt;Install dependencies in the Magento root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; grunt-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a bundle config targeting page types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example: split bundles by page type&lt;/span&gt;
&lt;span class="nx"&gt;bundles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bundles/default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jquery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jquery/ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mage/apply/main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mage/apply/scripts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mage/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mage/dataPost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mage/bootstrap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="nx"&gt;create&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bundles/catalog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Magento_Catalog/js/view/compare&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Magento_Catalog/js/price-box&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Magento_Catalog/js/product/view/product.types.config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bundles/default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;create&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;Run the bundling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;grunt &lt;span class="nb"&gt;exec&lt;/span&gt;:mage-build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces smaller, page-type-specific bundles so visitors only download what they need per page type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Defer Non-Critical JavaScript
&lt;/h2&gt;

&lt;p&gt;Not all JS needs to block rendering. Use Magento's layout XML to defer scripts that don't need to run at parse time.&lt;/p&gt;

&lt;p&gt;In your theme's &lt;code&gt;default.xml&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="nt"&gt;&amp;lt;block&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Framework\View\Element\Html\Head\Script"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"my.deferred.script"&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;"file"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;js/my-script.js&lt;span class="nt"&gt;&amp;lt;/argument&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;"attributes"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;defer&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;For third-party blocks loaded in the footer, adding &lt;code&gt;defer&lt;/code&gt; or &lt;code&gt;async&lt;/code&gt; can shave hundreds of milliseconds off your Largest Contentful Paint (LCP).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Lazy-Load Heavy Widgets
&lt;/h2&gt;

&lt;p&gt;RequireJS's &lt;code&gt;require()&lt;/code&gt; call is already lazy by nature, but make sure you're not eagerly loading heavy modules on every page. Audit your &lt;code&gt;requirejs-config.js&lt;/code&gt; for &lt;code&gt;deps&lt;/code&gt; entries — these are loaded eagerly on every page load regardless of whether the functionality is used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BAD — loads on every page&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Magento_SomeModule/js/heavy-widget&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// GOOD — only load when the DOM element exists&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heavyWidget&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Magento_SomeModule/js/heavy-widget&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then trigger the load conditionally in your template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heavyWidget&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only runs when explicitly required&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8: Remove Unused Modules' JS
&lt;/h2&gt;

&lt;p&gt;If you've disabled frontend modules (checkout steps, wishlists, product comparison), make sure their JavaScript isn't still being loaded. Use layout XML to remove blocks:&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;referenceBlock&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"some.js.block"&lt;/span&gt; &lt;span class="na"&gt;remove=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or disable the output of entire modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento module:disable Magento_Wishlist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every module you disable removes its RequireJS config contributions and associated JS files from the dependency graph.&lt;/p&gt;

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

&lt;p&gt;Use these tools to validate your work:&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 JS requests on a page&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://yourstore.com/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'src="[^"]*\.js"'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;

&lt;span class="c"&gt;# Or use Lighthouse CI&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @lhci/cli
lhci autorun &lt;span class="nt"&gt;--collect&lt;/span&gt;.url&lt;span class="o"&gt;=&lt;/span&gt;https://yourstore.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Target metrics after optimization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JS request count:&lt;/strong&gt; &amp;lt; 10 (down from 80–120)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total JS transfer size:&lt;/strong&gt; &amp;lt; 300KB gzipped for above-the-fold&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to Interactive (TTI):&lt;/strong&gt; &amp;lt; 3.5s on 4G&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bundling breaks checkout:&lt;/strong&gt; Magento's checkout is notoriously sensitive. Always test the full checkout flow after enabling bundling. If you see JavaScript errors, a module is likely missing from the bundle config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache must be flushed after every config change:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento cache:flush config full_page block_html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Static deploy must be re-run&lt;/strong&gt; after any &lt;code&gt;requirejs-config.js&lt;/code&gt; change — the merged config is written to &lt;code&gt;pub/static/&lt;/code&gt; at deploy time, not runtime.&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;Optimization&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;Production mode&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;Merge + minify&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;Built-in bundling&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;Advanced Grunt bundling&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;Defer non-critical scripts&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;Remove unused module JS&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;Lazy-load heavy widgets&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;JavaScript performance in Magento 2 isn't about one silver bullet — it's about layering these optimizations. Start with production mode and built-in bundling for immediate wins, then invest in advanced Grunt-based page-type bundling for the biggest long-term payoff.&lt;/p&gt;

&lt;p&gt;Every millisecond you shave off JS execution translates directly to better conversion rates. Google's own data shows a 0.1s improvement in load time correlates with an 8% improvement in conversion. That's not a rounding error — that's real revenue.&lt;/p&gt;

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