<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Magevanta</title>
    <description>The latest articles on DEV Community by Magevanta (@magevanta).</description>
    <link>https://dev.to/magevanta</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3887629%2F7af145fd-03e9-4362-99dc-a5f637f09ce1.png</url>
      <title>DEV Community: Magevanta</title>
      <link>https://dev.to/magevanta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/magevanta"/>
    <language>en</language>
    <item>
      <title>Magento 2 Page Builder Performance Optimization</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Mon, 03 Aug 2026 09:12:20 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-page-builder-performance-optimization-13k2</link>
      <guid>https://dev.to/magevanta/magento-2-page-builder-performance-optimization-13k2</guid>
      <description>&lt;p&gt;Magento Page Builder is één van de krachtigste tools voor contentbeheer in Adobe Commerce. Het stelt merchants in staat om rijke, visuele landingspagina's, productpagina's en CMS content te bouwen zonder een regel code te schrijven. Maar die flexibiliteit komt met een prijs: Page Builder introduceert meerdere performance bottlenecks die — indien onopgemerkt — flink impact kunnen hebben op pagina-laadtijden en server load.&lt;/p&gt;

&lt;p&gt;In deze post duik ik in de architectuur van Page Builder, identificeer ik de meest voorkomende performance problemen, en geef ik je concrete oplossingen om de rendering pipeline te versnellen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wat is Magento Page Builder?
&lt;/h2&gt;

&lt;p&gt;Page Builder is een drag-and-drop content editing tool die sinds Magento 2.3.1 standaard beschikbaar is in Adobe Commerce (en als beta in Open Source tot 2.4.3). Content elementen zoals banners, sliders, product grids, en tekstblokken worden opgebouwd via een visuele UI en opgeslagen als gestructureerde JSON-data in de database.&lt;/p&gt;

&lt;p&gt;Wanneer een pagina gerenderd wordt, converteert Magento deze JSON-data naar HTML via een rendering pipeline die bestaat uit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Content parsing&lt;/strong&gt; — JSON-decodering en structuurvalidatie&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Element rendering&lt;/strong&gt; — Per content element een renderer instantiëren&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Widget processing&lt;/strong&gt; — CMS widgets en directives uitvoeren&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout injection&lt;/strong&gt; — Gegenereerde HTML injecteren in de page layout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend asset loading&lt;/strong&gt; — CSS/JS voor Page Builder elementen laden&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Elke stap introduceert potentiële overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  De verborgen bottlenecks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Overhead door JSON-parsing en content tree traversing
&lt;/h3&gt;

&lt;p&gt;Page Builder slaat content op als complexe geneste JSON-structuur. Bij elke pagina-aanvraag decodeert Magento deze JSON, valideert de structuur, en doorloopt de boom recursief om HTML te genereren. Voor pagina's met veel elementen — denk aan lange landingspagina's met 50+ content blocks — is dit merkbaar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact:&lt;/strong&gt; CPU-intensief op elke request, geen native caching van de geparseerde structuur.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Gebruik Blackfire of New Relic en zoek naar &lt;code&gt;Magento\PageBuilder\Model\...&lt;/code&gt; methoden in de call tree.&lt;/li&gt;
&lt;li&gt;Let op &lt;code&gt;json_decode&lt;/code&gt;, &lt;code&gt;unserialize&lt;/code&gt;, en recursieve &lt;code&gt;render&lt;/code&gt;-calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Gebrek aan Full Page Cache voor dynamische Page Builder content
&lt;/h3&gt;

&lt;p&gt;Page Builder elementen zoals product carrousels, recent bekeken producten, en catalog price rules zijn inherent dynamisch. Standaard FPC (Varnish/built-in) cached de volledige pagina, maar als Page Builder content per gebruiker varieert — bijvoorbeeld door customer group specifieke prijzen — raakt de cache vaak gemarkeerd als uncacheable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Pagina's die normaal uit cache geserveerd worden, moeten volledig opnieuw gerenderd worden. Bij hoge traffic leidt dit tot database load spikes.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Check &lt;code&gt;X-Magento-Cache-Debug&lt;/code&gt; headers (in developer mode) of Varnish logs.&lt;/li&gt;
&lt;li&gt;Zoek naar &lt;code&gt;X-Magento-Vary: Customer-Group&lt;/code&gt; en andere vary-headers die cache misses veroorzaken.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Onnodige layout updates en block rendering
&lt;/h3&gt;

&lt;p&gt;Page Builder injecteert HTML in CMS blocks en pages via &lt;code&gt;afterLoad&lt;/code&gt;- en &lt;code&gt;beforeToHtml&lt;/code&gt;-observers. Als een pagina meerdere CMS blocks bevat die elk Page Builder content laden, worden deze observers meerdere keren getriggerd — soms met redundante database queries voor dezelfde content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Meerdere queries voor identieke EAV-attributen, block instantiatie overhead.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Query log analyse: zoek naar herhaalde &lt;code&gt;cms_block&lt;/code&gt; en &lt;code&gt;cms_page&lt;/code&gt; selects.&lt;/li&gt;
&lt;li&gt;Observer profiling: check &lt;code&gt;pagebuilder_load_content&lt;/code&gt; en gerelateerde events.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Frontend asset bloat
&lt;/h3&gt;

&lt;p&gt;Page Builder laadt standaard een set CSS- en JavaScript-bestanden voor de frontend rendering, inclusief jQuery UI componenten, TinyMCE-editor assets (ook op frontend!), en theming styles. Niet alle pagina's gebruiken alle elementen, maar de assets worden toch ingeladen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Grotere bundle sizes, extra HTTP requests (zonder bundling), langere Time-to-Interactive.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Chrome DevTools → Network tab: filter op &lt;code&gt;pagebuilder&lt;/code&gt; in asset URLs.&lt;/li&gt;
&lt;li&gt;Lighthouse audit: check "Reduce unused CSS/JS" opportunities.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Optimalisatiestrategieën
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cache de geparseerde content tree
&lt;/h3&gt;

&lt;p&gt;De duurste operatie bij Page Builder rendering is het recursief verwerken van de JSON-structuur. Je kunt dit cachen met een custom plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RenderContent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$cacheTags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;\Magento\Cms\Model\Block&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="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="nc"&gt;\Magento\Framework\Cache\FrontendInterface&lt;/span&gt; &lt;span class="nv"&gt;$cache&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;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$cache&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;aroundRender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Magento\PageBuilder\Model\Stage\Renderer&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callable&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pagebuilder_tree_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cacheTags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Let op: deze cache moet je invalideren bij CMS content updates. Gebruik Magento's cache invalidation mechanismen of voeg een &lt;code&gt;cleanCache&lt;/code&gt; plugin toe op de CMS block/page save methods.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Granular caching met ESI voor dynamische elementen
&lt;/h3&gt;

&lt;p&gt;In plaats van de hele pagina als uncacheable te markeren, configureer je Varnish met Edge Side Includes (ESI) voor specifieke Page Builder elementen die dynamisch zijn.&lt;/p&gt;

&lt;p&gt;Stappen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identificeer welke Page Builder elementen per request variëren (prijzen, stock status, customer group content).&lt;/li&gt;
&lt;li&gt;Wrap deze elementen in ESI tags via een custom renderer.&lt;/li&gt;
&lt;li&gt;Configureer Varnish om ESI-requests afzonderlijk te cachen met kortere TTL.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In je custom Page Builder renderer&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="nf"&gt;isDynamicElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$element&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="s1"&gt;'&amp;lt;esi:include src="/pagebuilder/dynamic/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$element&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'" /&amp;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;Dit houdt de core pagina-cache intact terwijl alleen de dynamische fragmenten per request gerenderd worden.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimaliseer asset loading
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Verwijder ongebruikte Page Builder assets:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Page Builder laadt &lt;code&gt;Magento_PageBuilder/js/stage-builder&lt;/code&gt; en gerelateerde scripts ook op de frontend. Dit is editor-code die niet nodig is voor rendering. Je kunt dit verwijderen via &lt;code&gt;requirejs-config.js&lt;/code&gt; of een layout update:&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 je theme's default.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;page&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;remove&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"Magento_PageBuilder::js/stage-builder.js"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;remove&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"Magento_PageBuilder::js/resource-slider.js"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/head&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;blockquote&gt;
&lt;p&gt;Let op: test grondig — sommige frontend-interacties (zoals accordion/tabs) vereisen specifieke scripts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Implementeer Critical CSS:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laad alleen de Page Builder CSS die boven de vouw nodig is inline, en defer de rest. Magento's native CSS critical path tooling werkt hier goed samen met Page Builder's &lt;code&gt;styles.css&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database optimalisaties
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Voeg indexen toe op cms_page.content en cms_block.content:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Page Builder content wordt opgeslagen in &lt;code&gt;content&lt;/code&gt; kolommen als JSON. Alhoewel je niet direct op JSON velden filtert in SQL, kunnen full-table scans optreden bij CMS entity loading als er geen efficiënte indexen zijn op de primaire keys en store-scoped lookups.&lt;/p&gt;

&lt;p&gt;Controleer je queries en voeg composite indexen toe als je store-scoped CMS content laadt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;cms_page&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IDX_CMS_PAGE_STORE_SCOPE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;cms_block&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IDX_CMS_BLOCK_STORE_SCOPE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Archiveer oude Page Builder revisies:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Adobe Commerce slaat content revisies op in &lt;code&gt;sequence_pagebuilder_content&lt;/code&gt;. Bij langdurig gebruik groeit deze tabel. Voeg een cleanup cron toe:&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;// Cleanup cron job&lt;/span&gt;
&lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTableName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sequence_pagebuilder_content'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'created_at &amp;lt; ?'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$date&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;subMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&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;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Y-m-d H:i:s'&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;
  
  
  Vermijd Page Builder voor data-intensieve elementen
&lt;/h3&gt;

&lt;p&gt;Product carrousels en grids in Page Builder zijn handig voor marketeers, maar renderen vaak langzamer dan native Magento widgets of blokken met efficiëntere caching. Overweeg om:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product grids te vervangen door custom widgets met dedicated cache tags&lt;/li&gt;
&lt;li&gt;Recent bekeken producten uit Page Builder te halen en als standalone blok te implementeren met &lt;code&gt;X-Magento-Vary&lt;/code&gt;-optimalisatie&lt;/li&gt;
&lt;li&gt;Statische banners in Page Builder te houden, dynamische data in aparte blokken met eigen cache lifecycle&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Monitoring en debugging
&lt;/h2&gt;

&lt;p&gt;Houd deze metrics in de gaten na optimalisatie:&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;Tool&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Page Builder render time&lt;/td&gt;
&lt;td&gt;Blackfire / New Relic&lt;/td&gt;
&lt;td&gt;&amp;lt; 50ms per page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache hit ratio&lt;/td&gt;
&lt;td&gt;Varnishstat / Magento logs&lt;/td&gt;
&lt;td&gt;&amp;gt; 95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend asset weight&lt;/td&gt;
&lt;td&gt;Lighthouse&lt;/td&gt;
&lt;td&gt;&amp;lt; 200KB Page Builder CSS/JS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CMS query count&lt;/td&gt;
&lt;td&gt;Magento Profiler&lt;/td&gt;
&lt;td&gt;&amp;lt; 5 queries per block&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusie
&lt;/h2&gt;

&lt;p&gt;Page Builder is een geweldig tool voor content teams, maar vereist bewustzijn van de performance-implicaties. Door de content tree te cachen, ESI te gebruiken voor dynamische elementen, assets op te schonen, en database queries te optimaliseren, behoud je de flexibiliteit zonder je laadtijden op te offeren.&lt;/p&gt;

&lt;p&gt;De sleutel is granulariteit: cache wat statisch is, render dynamisch wat per request varieert, en monitor continu om regressies vroeg te vangen.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Heb je Page Builder performance issues ervaren? Deel je casus — ik help graag meedenken over specifieke scenario's.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>performance</category>
      <category>pagebuilder</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 API Rate Limiting: Protecting Your Store from Abuse</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Mon, 03 Aug 2026 09:06:27 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-api-rate-limiting-protecting-your-store-from-abuse-2pa4</link>
      <guid>https://dev.to/magevanta/magento-2-api-rate-limiting-protecting-your-store-from-abuse-2pa4</guid>
      <description>&lt;p&gt;Your Magento 2 REST and GraphQL APIs are public-facing endpoints that power everything from mobile apps to third-party integrations. Left unprotected, they become easy targets for abuse: scrapers hammering your catalog endpoints, brute-force attacks against customer accounts, or competitors systematically draining your inventory data. Rate limiting is the defensive layer most stores skip — until it's too late.&lt;/p&gt;

&lt;p&gt;This guide covers practical rate limiting strategies for Magento 2, from built-in mechanisms to custom middleware, with production-ready configuration examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Rate Limiting Matters for Magento 2
&lt;/h2&gt;

&lt;p&gt;Magento 2 exposes extensive APIs by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;REST API&lt;/strong&gt;: &lt;code&gt;/rest/V1/...&lt;/code&gt; — products, orders, customers, inventory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GraphQL&lt;/strong&gt;: &lt;code&gt;/graphql&lt;/code&gt; — storefront queries, mutations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin REST&lt;/strong&gt;: OAuth-protected backend endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without throttling, a single malicious client or misconfigured integration can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exhaust PHP-FPM worker pools, causing 502/504 errors for real shoppers&lt;/li&gt;
&lt;li&gt;Trigger database lock contention during bulk catalog reads&lt;/li&gt;
&lt;li&gt;Hit Elasticsearch/OpenSearch with expensive aggregation queries&lt;/li&gt;
&lt;li&gt;Deplete Redis connection pools or memory with uncached repeated requests&lt;/li&gt;
&lt;li&gt;Reveal sensitive business data through systematic enumeration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost isn't just infrastructure — it's lost revenue when legitimate checkout traffic gets squeezed out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built-In Magento 2 API Throttling
&lt;/h2&gt;

&lt;h3&gt;
  
  
  REST API Rate Limits (WebAPI Framework)
&lt;/h3&gt;

&lt;p&gt;Magento 2's WebAPI layer has configurable rate limiting via &lt;code&gt;env.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/etc/env.php&lt;/span&gt;
&lt;span class="s1"&gt;'system'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'webapi'&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;'rest'&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;'max_requests'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'time_period'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;  &lt;span class="c1"&gt;// seconds&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'graphql'&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;'max_requests'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'time_period'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important caveat&lt;/strong&gt;: This built-in rate limiter is &lt;strong&gt;per-IP by default&lt;/strong&gt; and applies globally across all REST endpoints. It's a blunt instrument: a customer browsing via mobile app on a shared office IP hits the same ceiling as a scraper. Production stores need more nuanced control.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebAPI Throttling per Integration
&lt;/h3&gt;

&lt;p&gt;For OAuth-connected integrations, you can set custom limits:&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;'system'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'webapi'&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;'throttling'&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;'max_requests'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'time_period'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'integrations'&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;'my_erp_integration'&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;'max_requests'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'time_period'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
                &lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="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;This lets legitimate high-volume partners (ERPs, WMSs) operate without triggering blanket blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Rate Limiting with Magento 2 Plugins
&lt;/h2&gt;

&lt;p&gt;For granular, endpoint-specific throttling, implement a custom plugin on &lt;code&gt;\Magento\Webapi\Controller\Rest&lt;/code&gt; or &lt;code&gt;\Magento\GraphQl\Controller\GraphQl&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis-Backed Sliding Window Rate Limiter
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// app/code/Vendor/ApiThrottle/Plugin/RestApiRateLimiter.php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Vendor\ApiThrottle\Plugin&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\Cache\FrontendInterface&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\Webapi\Controller\Rest&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;RestApiRateLimiter&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;FrontendInterface&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$maxRequests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$windowSeconds&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="kt"&gt;FrontendInterface&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$maxRequests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$windowSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="err"&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;=&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="err"&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;maxRequests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$maxRequests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="err"&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;windowSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$windowSeconds&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;beforeDispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Rest&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$clientKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&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;getClientIdentifier&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&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;getEndpointIdentifier&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api_limit_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;\$clientKey}_{\$endpoint&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&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;getSlidingWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&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="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$requests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="err"&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;maxRequests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\Magento\Framework\Webapi\Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Rate limit exceeded. Try again in %1 seconds.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&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;getRetryAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$requests&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="nc"&gt;\Magento\Framework\Webapi\Exception&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP_TOO_MANY_REQUESTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Retry-After'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&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;getRetryAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$requests&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="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$requests&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="err"&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="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$requests&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="err"&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="err"&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;windowSeconds&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getSlidingWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&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="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="err"&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;windowSeconds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;array_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$cutoff&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getClientIdentifier&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_X_FORWARDED_FOR'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REMOTE_ADDR'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'unknown'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_X_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$ip&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getEndpointIdentifier&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;parse_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REQUEST_URI'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;PHP_URL_PATH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/[^a-z0-9]/'&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="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getRetryAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$requests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;int&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$requests&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="err"&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;windowSeconds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$oldest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$requests&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;max&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="err"&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;windowSeconds&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$oldest&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;This implements a &lt;strong&gt;sliding window&lt;/strong&gt; algorithm using Magento's cache (Redis-backed in production). Each client+endpoint combination gets its own counter, preventing one noisy endpoint from blocking an entire integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  GraphQL Query Complexity Throttling
&lt;/h3&gt;

&lt;p&gt;GraphQL's flexibility is its Achilles' heel: a single query can request thousands of nested objects. Limit by query complexity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// app/code/Vendor/ApiThrottle/Plugin/GraphQlComplexityLimiter.php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Vendor\ApiThrottle\Plugin&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\GraphQl\Controller\GraphQl&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;GraphQlComplexityLimiter&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$maxComplexity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$maxDepth&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="kt"&gt;int&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$maxComplexity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$maxDepth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="err"&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;maxComplexity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$maxComplexity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="err"&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;maxDepth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$maxDepth&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;beforeDispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;GraphQl&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'php://input'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s1"&gt;'query'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$complexity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&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;calculateComplexity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&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;calculateDepth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$query&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="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$complexity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&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;maxComplexity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\Magento\Framework\GraphQl\Exception\GraphQlInputException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s1"&gt;'Query complexity %1 exceeds maximum %2. '&lt;/span&gt;
                    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'Reduce nested fields or add pagination.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$complexity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="err"&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;maxComplexity&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$depth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&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;maxDepth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\Magento\Framework\GraphQl\Exception\GraphQlInputException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Query depth %1 exceeds maximum %2.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&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;maxDepth&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="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;calculateComplexity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;int&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$complexity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// Simple heuristic: count product/category references × potential children&lt;/span&gt;
        &lt;span class="nb"&gt;preg_match_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/\\b(products|categories|items|children)\\b/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$matches&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$complexity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Add for pagination without pageSize&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'products'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;str_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pageSize'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$complexity&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Penalize unbounded product queries&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$complexity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;calculateDepth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;int&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$maxDepth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$char&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="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$char&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'{'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$depth&lt;/span&gt;&lt;span class="o"&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="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$char&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'}'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$depth&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$maxDepth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$maxDepth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$depth&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="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$maxDepth&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;h2&gt;
  
  
  Nginx-Level Rate Limiting (First Line of Defense)
&lt;/h2&gt;

&lt;p&gt;Stop abuse before it reaches PHP-FPM. Nginx &lt;code&gt;limit_req&lt;/code&gt; is faster and more reliable than application-level throttling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /etc/nginx/conf.d/magento_rate_limits.conf&lt;/span&gt;

&lt;span class="c1"&gt;# Define shared memory zone: 10MB, rate 10r/s per IP&lt;/span&gt;
&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=api:10m&lt;/span&gt; &lt;span class="s"&gt;rate=10r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Stricter zone for write endpoints&lt;/span&gt;
&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=api_write:10m&lt;/span&gt; &lt;span class="s"&gt;rate=2r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Per-integration key zone (read from header or query param)&lt;/span&gt;
&lt;span class="k"&gt;map&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$http_x_api_key&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$api_limit_key&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;default&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;""&lt;/span&gt;      &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;~.+&lt;/span&gt;     &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$http_x_api_key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$api_limit_key&lt;/span&gt; &lt;span class="s"&gt;zone=integration:50m&lt;/span&gt; &lt;span class="s"&gt;rate=50r/s&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="c1"&gt;# Apply to all REST endpoints&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;^/rest/V1/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=api&lt;/span&gt; &lt;span class="s"&gt;burst=20&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# Stricter for mutations&lt;/span&gt;
        &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;^/rest/V1/(orders|invoices|shipments|creditmemos)/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=api_write&lt;/span&gt; &lt;span class="s"&gt;burst=5&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;# Higher limit for authenticated integrations&lt;/span&gt;
        &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$http_authorization&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;"Bearer")&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=integration&lt;/span&gt; &lt;span class="s"&gt;burst=100&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.php\&lt;/span&gt;&lt;span class="nv"&gt;$is_args&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# GraphQL endpoint&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/graphql&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=api&lt;/span&gt; &lt;span class="s"&gt;burst=30&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# POST mutations are more expensive&lt;/span&gt;
        &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$request_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;POST)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=api_write&lt;/span&gt; &lt;span class="s"&gt;burst=10&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.php\&lt;/span&gt;&lt;span class="nv"&gt;$is_args&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;nodelay&lt;/code&gt; parameter is critical: without it, Nginx queues excess requests instead of rejecting them immediately, which can backpressure your application during a spike.&lt;/p&gt;

&lt;h2&gt;
  
  
  Varnish Rate Limiting for Cached API Responses
&lt;/h2&gt;

&lt;p&gt;For read-heavy REST endpoints that hit Varnish (products, categories), add rate limiting in VCL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;// /etc/varnish/default.vcl&lt;/span&gt;

&lt;span class="k"&gt;sub&lt;/span&gt; &lt;span class="nf"&gt;vcl_recv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# API endpoint detection&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.url&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s2"&gt;"^/rest/V1/products"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;req.url&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s2"&gt;"^/rest/V1/categories"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;# Check rate limit header from backend&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;std.integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.http.X-RateLimit-Remaining&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="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;synth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Too Many Requests"&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;Alternatively, use Varnish's &lt;code&gt;vmod_vsthrottle&lt;/code&gt; for in-Varnish rate limiting without backend roundtrips:&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;import&lt;/span&gt; vsthrottle&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;sub&lt;/span&gt; &lt;span class="nf"&gt;vcl_recv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.url&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s2"&gt;"^/(rest|graphql)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;vsthrottle.is_allowed&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"api:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;client.ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10r/s"&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;&lt;span class="nf"&gt;synth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Rate limit exceeded"&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;h2&gt;
  
  
  Per-Customer vs. Per-IP: Choosing the Right Granularity
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Granularity&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Drawback&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Per-IP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Guest API traffic, DDoS protection&lt;/td&gt;
&lt;td&gt;Shared NAT (offices, mobile carriers) causes false positives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Per-API-key&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Authenticated integrations, mobile apps&lt;/td&gt;
&lt;td&gt;Requires clients to send consistent identifiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Per-customer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Logged-in storefront GraphQL&lt;/td&gt;
&lt;td&gt;Session lookup adds latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Per-endpoint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mixed API surface (read vs. write)&lt;/td&gt;
&lt;td&gt;More complex configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For production, combine layers: Nginx per-IP for DDoS, application per-key for authenticated traffic, and per-endpoint complexity limits for GraphQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring and Alerting on API Abuse
&lt;/h2&gt;

&lt;p&gt;Don't fly blind. Track these metrics via New Relic, Datadog, or Magento's built-in reporting:&lt;/p&gt;

&lt;h3&gt;
  
  
  Log Format (customize in &lt;code&gt;env.php&lt;/code&gt;)
&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;'log'&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;'handlers'&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;'api_rate_limit'&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;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'file'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'file'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/var/log/magento/api_rate_limit.log'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'level'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\Monolog\Logger&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WARNING&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;
  
  
  What to Alert On
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;429 responses &amp;gt; 1% of total API traffic&lt;/strong&gt; — indicates overly aggressive limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;429 responses from single IP &amp;gt; 100/hour&lt;/strong&gt; — likely scraper or attack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GraphQL complexity rejections &amp;gt; 5%&lt;/strong&gt; — mobile app or headless frontend needs optimization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p99 API latency spike coinciding with 429s&lt;/strong&gt; — rate limiter may be queuing instead of rejecting&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Production Checklist
&lt;/h2&gt;

&lt;p&gt;Before deploying rate limiting to production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Baseline your traffic&lt;/strong&gt; — log current request rates per endpoint for 7 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set limits 3× above peak legitimate traffic&lt;/strong&gt; — give headroom for sales events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement graduated responses&lt;/strong&gt; — return &lt;code&gt;429&lt;/code&gt; with &lt;code&gt;Retry-After&lt;/code&gt; header; don't silently drop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whitelist load balancers and health checks&lt;/strong&gt; — &lt;code&gt;\$binary_remote_addr&lt;/code&gt; sees the LB, not the client&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with your actual integrations&lt;/strong&gt; — ERP sync, mobile app, headless frontend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor 429s in real-time&lt;/strong&gt; — have a runbook to temporarily raise limits if false positives spike&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document your limits&lt;/strong&gt; — share rate limits with integration partners to prevent surprise blocks&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Rate limiting in Magento 2 is not a single switch — it's a layered defense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nginx&lt;/strong&gt;: Cheap, fast, first line of defense against volume attacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Varnish&lt;/strong&gt;: Protect cached endpoints without backend load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application&lt;/strong&gt;: Granular, context-aware (per-integration, per-endpoint, per-complexity)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in WebAPI&lt;/strong&gt;: Simple global limits as a safety net&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with Nginx for immediate protection, then add application-level complexity limits for GraphQL, and finally tune per-integration allowances for your ERP and mobile app partners. The goal isn't to block traffic — it's to ensure a single bad actor can't ruin the experience for everyone else.&lt;/p&gt;

&lt;p&gt;Have you implemented API throttling in your Magento 2 store? What rate limiting strategy worked best for your traffic profile?&lt;/p&gt;

</description>
      <category>magento</category>
      <category>security</category>
      <category>performance</category>
    </item>
    <item>
      <title>Magento 2 Customer Data Sections &amp; localStorage Performance Optimization</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sat, 01 Aug 2026 09:16:56 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-customer-data-sections-localstorage-performance-optimization-4ci9</link>
      <guid>https://dev.to/magevanta/magento-2-customer-data-sections-localstorage-performance-optimization-4ci9</guid>
      <description>&lt;h1&gt;
  
  
  Magento 2 Customer Data Sections &amp;amp; localStorage Performance Optimization
&lt;/h1&gt;

&lt;p&gt;Every Magento 2 storefront uses &lt;strong&gt;customer data sections&lt;/strong&gt; — the mechanism behind the mini-cart, customer name display, wishlist counters, and checkout summaries. It looks seamless to shoppers, but under the hood it can silently murder your page load performance.&lt;/p&gt;

&lt;p&gt;If you've ever wondered why your pages fire an extra AJAX request immediately after the initial load, or why your localStorage balloons to several megabytes, this post is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Customer Data Sections?
&lt;/h2&gt;

&lt;p&gt;Magento 2 splits page rendering into two phases: &lt;strong&gt;server-side&lt;/strong&gt; (Astro/Varnish/FPC) and &lt;strong&gt;client-side&lt;/strong&gt; (JavaScript). Because full-page cache serves the same HTML to every visitor, personalized data — cart contents, logged-in customer name, wishlist count — cannot be rendered server-side for cached pages.&lt;/p&gt;

&lt;p&gt;Enter &lt;code&gt;sections.xml&lt;/code&gt; and the &lt;strong&gt;Customer Data JS API&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Vendor_Module/etc/frontend/sections.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:module:Magento_Customer:etc/sections.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&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;"checkout/cart/add"&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="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"checkout-data"&lt;/span&gt;&lt;span class="nt"&gt;/&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;p&gt;This file tells Magento: &lt;em&gt;"When the &lt;code&gt;checkout/cart/add&lt;/code&gt; action runs, invalidate the &lt;code&gt;cart&lt;/code&gt; and &lt;code&gt;checkout-data&lt;/code&gt; sections."&lt;/em&gt; On the next page load, JavaScript detects these invalidated sections and fetches fresh data via &lt;code&gt;customer/section/load/&lt;/code&gt; AJAX.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Data Flow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Page loads&lt;/strong&gt; from FPC/Varnish (no personalization)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS initializes&lt;/strong&gt; &lt;code&gt;Magento_Customer/js/customer-data&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;localStorage&lt;/strong&gt; is checked for cached section data (by &lt;code&gt;sectionLoadUrl&lt;/code&gt; + &lt;code&gt;storeId&lt;/code&gt; key)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalidated sections&lt;/strong&gt; trigger &lt;code&gt;POST /customer/section/load/&lt;/code&gt; with section names&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response updates&lt;/strong&gt; localStorage, ko.observables, and UI (mini-cart, messages, etc.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This sounds efficient, but it has &lt;strong&gt;three major performance traps&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Trap #1: The "Sections Hell" AJAX Request
&lt;/h2&gt;

&lt;p&gt;Out of the box, Magento 2's &lt;code&gt;customer-data&lt;/code&gt; module calls &lt;code&gt;customer/section/load/&lt;/code&gt; with &lt;strong&gt;every registered section&lt;/strong&gt; on the first uncached hit:&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;// vendor/magento/module-customer/view/frontend/web/js/customer-data.js&lt;/span&gt;
&lt;span class="c1"&gt;// (simplified)&lt;/span&gt;
&lt;span class="nx"&gt;getFromServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sectionNames&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="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer/section/load/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sectionNames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;update_section_id&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;A typical Magento installation registers &lt;strong&gt;15–25 sections&lt;/strong&gt;: &lt;code&gt;cart&lt;/code&gt;, &lt;code&gt;checkout-data&lt;/code&gt;, &lt;code&gt;comparison&lt;/code&gt;, &lt;code&gt;directory-data&lt;/code&gt;, &lt;code&gt;customer&lt;/code&gt;, &lt;code&gt;wishlist&lt;/code&gt;, &lt;code&gt;messages&lt;/code&gt;, &lt;code&gt;last-ordered-items&lt;/code&gt;, &lt;code&gt;review&lt;/code&gt;, &lt;code&gt;product_data_storage&lt;/code&gt;, &lt;code&gt;recently_viewed_product&lt;/code&gt;, &lt;code&gt;recently_compared_product&lt;/code&gt;, &lt;code&gt;paypalbilling_agreement&lt;/code&gt;, &lt;code&gt;persistent&lt;/code&gt;, and any custom sections added by extensions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Each section requires database queries&lt;/strong&gt; on the backend:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;Typical Backend Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Quote load + items + totals calculation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;customer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Customer entity load + address collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wishlist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wishlist item collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;directory-data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Country/region data + shipping rates cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;checkout-data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Quote address validation + shipping method resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On a high-traffic store, that single &lt;code&gt;section/load/&lt;/code&gt; call can take &lt;strong&gt;300ms–1.2s&lt;/strong&gt; depending on cart complexity, catalog size, and whether Redis is configured for session storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Kicker: It Runs on Every Page
&lt;/h3&gt;

&lt;p&gt;Unlike cart/checkout pages (where you expect some backend work), this request fires on &lt;strong&gt;every cached CMS page, category page, and product page&lt;/strong&gt;. A shopper browsing 20 products generates &lt;strong&gt;20 section/load requests&lt;/strong&gt; — all doing the same work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Trap #2: localStorage Bloat
&lt;/h2&gt;

&lt;p&gt;Section data is stored in the browser's &lt;code&gt;localStorage&lt;/code&gt;. Magento's default key format is:&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="nx"&gt;mage&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;     &lt;span class="c1"&gt;// ~50KB–500KB+&lt;/span&gt;
&lt;span class="nx"&gt;mage&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;section&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;invalidation&lt;/span&gt;  &lt;span class="c1"&gt;// small&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;cart&lt;/code&gt; section alone can grow to &lt;strong&gt;100KB+&lt;/strong&gt; if the shopper has a complex cart with configurable products, custom options, and tier pricing. Multiply by 20 sections and you're pushing &lt;strong&gt;1–2MB of localStorage per visitor&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This causes three problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mobile browsers&lt;/strong&gt; aggressively evict localStorage. On iOS Safari, storage can be cleared on memory pressure, forcing a full re-fetch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow deserialization&lt;/strong&gt; — &lt;code&gt;JSON.parse()&lt;/code&gt; on 1MB+ strings blocks the main thread for 10–50ms, contributing to poor Interaction to Next Paint (INP).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session storage quota&lt;/strong&gt; — some browsers limit &lt;code&gt;localStorage&lt;/code&gt; to 5MB. One heavy cart can approach the limit, breaking other features.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Performance Trap #3: Section Invalidation Abuse
&lt;/h2&gt;

&lt;p&gt;The most common mistake: &lt;strong&gt;declaring section invalidation on every controller action&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- BAD: Extension invalidates everything on every page --&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;"*"&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="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"customer"&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;"wishlist"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/action&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some third-party extensions use wildcard (&lt;code&gt;*&lt;/code&gt;) matching or invalidate &lt;code&gt;cart&lt;/code&gt; on non-cart actions (like product list page views via a tracking pixel). This forces the &lt;code&gt;section/load/&lt;/code&gt; call on &lt;strong&gt;every single page&lt;/strong&gt; even when nothing changed.&lt;/p&gt;

&lt;p&gt;Another abuse pattern: plugins on &lt;code&gt;getSectionData()&lt;/code&gt; that run &lt;strong&gt;expensive queries&lt;/strong&gt; for data that isn't even displayed on the current page.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  1. Audit Your sections.xml Files
&lt;/h3&gt;

&lt;p&gt;First, map out what you're actually invalidating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find all section definitions&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;"&amp;lt;action name="&lt;/span&gt; app/code vendor/magento &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sections.xml"&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;# Find wildcard invalidation (highly suspicious)&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;action name="\*"'&lt;/span&gt; app/code vendor &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sections.xml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each wildcard or overly broad invalidation, ask: &lt;em&gt;"Does this action actually change this section's data?"&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Eliminate Unused Sections from Initial Load
&lt;/h3&gt;

&lt;p&gt;You can configure which sections load on the initial request via &lt;code&gt;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="c"&gt;&amp;lt;!-- app/etc/di.xml or your module --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Customer\CustomerData\SectionPoolInterface"&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;"sectionSourceMap"&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="c"&gt;&amp;lt;!-- Only load what you actually display on every page --&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;"cart"&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;Magento\Checkout\CustomerData\Cart&lt;span class="nt"&gt;&amp;lt;/item&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;"customer"&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;Magento\Customer\CustomerData\Customer&lt;span class="nt"&gt;&amp;lt;/item&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;"messages"&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;Magento\Theme\CustomerData\Messages&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;/type&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sections like &lt;code&gt;recently_viewed_product&lt;/code&gt;, &lt;code&gt;recently_compared_product&lt;/code&gt;, and &lt;code&gt;review&lt;/code&gt; rarely need to be in the initial &lt;code&gt;section/load/&lt;/code&gt; call. Load them lazily when the relevant widget initializes.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Disable Sections You Don't Use
&lt;/h3&gt;

&lt;p&gt;If you're not using wishlist or product comparison, remove them entirely:&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;!-- app/code/Vendor/Module/etc/frontend/di.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Customer\CustomerData\SectionPool"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;plugin&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"disable_unused_sections"&lt;/span&gt;
            &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"Vendor\Module\Plugin\DisableUnusedSections"&lt;/span&gt;
            &lt;span class="na"&gt;sortOrder=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Vendor\Module\Plugin&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;DisableUnusedSections&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$disabledSections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'wishlist'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'comparison'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'review'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'last-ordered-items'&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;afterGetSectionNames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Magento\Customer\CustomerData\SectionPool&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;array_diff&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;disabledSections&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;This removes those sections from both the &lt;code&gt;section/load/&lt;/code&gt; call and &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Switch to sessionStorage for Ephemeral Data
&lt;/h3&gt;

&lt;p&gt;For sections that don't need to persist across tabs (like &lt;code&gt;messages&lt;/code&gt;), override the storage backend:&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;!-- app/code/Vendor/Module/etc/frontend/di.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Customer\CustomerData\JsLayoutDataProviderPool"&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;"components"&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="c"&gt;&amp;lt;!-- Custom storage handler --&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;/type&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use a simpler approach: patch &lt;code&gt;customer-data.js&lt;/code&gt; to use &lt;code&gt;sessionStorage&lt;/code&gt; for sections that are tab-specific, reducing localStorage pressure.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Implement Server-Side Push (Advanced)
&lt;/h3&gt;

&lt;p&gt;Instead of the browser polling on every page load, push updates &lt;strong&gt;only when data actually changes&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;// In your observer, after cart modification&lt;/span&gt;
&lt;span class="nv"&gt;$sectionIdentifier&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;sectionIdentifierFactory&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;$sectionIdentifier&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;resetIdentifier&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Forces JS to re-fetch on next action&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better yet, for headless/API-first architectures, bypass &lt;code&gt;customer-data&lt;/code&gt; entirely and use &lt;strong&gt;GraphQL queries&lt;/strong&gt; for cart state, loading only what's needed when the mini-cart opens.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Redis for Section Metadata
&lt;/h3&gt;

&lt;p&gt;While section data itself is in the browser, the backend still queries the database. Ensure your &lt;code&gt;customer/section/load/&lt;/code&gt; endpoint uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis session storage&lt;/strong&gt; (not DB sessions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quote caching&lt;/strong&gt; via &lt;code&gt;Magento_Quote&lt;/code&gt; cache tags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Varnish pass-through&lt;/strong&gt; for &lt;code&gt;section/load/&lt;/code&gt; (do not cache this endpoint — it must be dynamic)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Use Blackfire or New Relic to trace &lt;code&gt;customer/section/load/&lt;/code&gt; calls:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open any cached product page&lt;/li&gt;
&lt;li&gt;Check the &lt;strong&gt;Network&lt;/strong&gt; tab — find the &lt;code&gt;section/load/&lt;/code&gt; POST&lt;/li&gt;
&lt;li&gt;Profile it — look for &lt;code&gt;QuoteRepository::get&lt;/code&gt;, &lt;code&gt;TotalsCollector::collect&lt;/code&gt;, &lt;code&gt;AddressRepository::getList&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Apply optimizations and re-profile&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Expected results after optimization:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Section count&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AJAX payload size&lt;/td&gt;
&lt;td&gt;45KB&lt;/td&gt;
&lt;td&gt;8KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;section/load/&lt;/code&gt; backend time&lt;/td&gt;
&lt;td&gt;450ms&lt;/td&gt;
&lt;td&gt;80ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;localStorage size&lt;/td&gt;
&lt;td&gt;1.2MB&lt;/td&gt;
&lt;td&gt;180KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INP improvement&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;-40ms&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;Customer data sections are a powerful but dangerous feature. The default Magento 2 configuration is &lt;strong&gt;generous to a fault&lt;/strong&gt; — it loads every section on every page, stores megabytes in localStorage, and assumes all extensions know how to invalidate responsibly.&lt;/p&gt;

&lt;p&gt;The fix is surgical: audit your &lt;code&gt;sections.xml&lt;/code&gt;, eliminate unused sections, disable features you don't need, and ensure your backend session/cache layer is optimized for the &lt;code&gt;customer/section/load/&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fastest AJAX request is the one you don't have to make.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found this helpful? Check out our &lt;a href="https://dev.to/blog/magento-2-performance-optimization-guide-2026"&gt;Magento 2 Performance Optimization Guide 2026&lt;/a&gt; for a full-storefront tuning strategy.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
    </item>
    <item>
      <title>Magento 2 Admin Grid Performance Optimization: Speed Up Your Backend</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Thu, 30 Jul 2026 09:05:36 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-admin-grid-performance-optimization-speed-up-your-backend-4nnk</link>
      <guid>https://dev.to/magevanta/magento-2-admin-grid-performance-optimization-speed-up-your-backend-4nnk</guid>
      <description>&lt;p&gt;Your frontend might be lightning-fast, but if your &lt;strong&gt;admin grids take 8+ seconds to load&lt;/strong&gt;, your team is bleeding hours every week. Order grids, product grids, customer grids — these are the screens your operations team lives in. Every extra second is friction, errors, and lost revenue.&lt;/p&gt;

&lt;p&gt;In this guide, you'll learn the root causes of slow admin grids in Magento 2 and the specific optimizations that bring grid load times from 8 seconds down to under 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Admin Grids Are So Slow
&lt;/h2&gt;

&lt;p&gt;Magento 2 admin grids are built on a flexible but expensive architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EAV joins for product/customer grids&lt;/strong&gt; — every attribute is a separate table join&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No frontend caching&lt;/strong&gt; — grids bypass the FPC entirely; every load is a fresh query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Massive default collections&lt;/strong&gt; — unfiltered grids load thousands of records with full attribute sets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy rendering&lt;/strong&gt; — each row triggers multiple template renders, image resizes, and status lookups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing database indexes&lt;/strong&gt; — out-of-the-box, critical grid filters aren't indexed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical unoptimized sales order grid with 50,000+ orders can fire 40+ SQL queries and take 6-10 seconds. Multiply that by 20 grid loads per day per admin, and you're looking at hours of waiting per week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimize the Grid Collection
&lt;/h2&gt;

&lt;p&gt;The collection is where most of the time is spent. Here's how to fix it.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add Custom Indexes for Grid Filters
&lt;/h3&gt;

&lt;p&gt;The out-of-the-box indexes don't cover common grid filter columns. Add composite indexes for your heaviest grids:&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;-- Sales order grid: status + created_at is the most common filter combo&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales_order_grid&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_status_created_at&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;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Product grid: name + status + visibility&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_name_status_visibility&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;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Limit Default Grid Page Size
&lt;/h3&gt;

&lt;p&gt;Edit the grid's UI component XML to reduce the default &lt;code&gt;pageSize&lt;/code&gt;. Magento defaults to 200 — way too high for production.&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;!-- app/code/Vendor/Module/view/adminhtml/ui_component/sales_order_grid.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;listing&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="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;settings&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;paging&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;options&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"20"&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;"value"&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;20&lt;span class="nt"&gt;&amp;lt;/item&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;"label"&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;20&lt;span class="nt"&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"50"&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;"value"&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;50&lt;span class="nt"&gt;&amp;lt;/item&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;"label"&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;50&lt;span class="nt"&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/options&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;pageSize&amp;gt;&lt;/span&gt;20&lt;span class="nt"&gt;&amp;lt;/pageSize&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- Default from 200 → 20 --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/paging&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/settings&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/listing&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Disable Expensive Columns by Default
&lt;/h3&gt;

&lt;p&gt;Remove or hide columns that trigger heavy lookups (thumbnail generation, full address formatting, custom attribute rendering):&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;!-- Remove the thumbnail column from product grid --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;columns&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"product_columns"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"thumbnail"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Catalog\Ui\Component\Listing\Columns\Thumbnail"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;settings&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;visible&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/visible&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/settings&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/column&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/columns&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Users can still toggle them on, but the default grid load skips the expensive work.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Override the Collection with Selective Loading
&lt;/h3&gt;

&lt;p&gt;For custom grids, override &lt;code&gt;getDataSourceData()&lt;/code&gt; to load only what you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/code/Vendor/Module/Ui/DataProvider/Order/GridDataProvider.php&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GridDataProvider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;\Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider&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;getData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&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;getData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Skip loading full address objects for every row&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'items'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'billing_address'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'shipping_address'&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;$data&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;h2&gt;
  
  
  Fix EAV Grid Performance
&lt;/h2&gt;

&lt;p&gt;Product and customer grids use EAV, which means every custom attribute triggers a LEFT JOIN:&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;-- What Magento does for a product grid with 10 custom attributes&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at_price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;at_status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at_visibility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;at_special_price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;special_price&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_varchar&lt;/span&gt; &lt;span class="n"&gt;at_name&lt;/span&gt; 
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;at_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;at_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;71&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_decimal&lt;/span&gt; &lt;span class="n"&gt;at_price&lt;/span&gt; 
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;at_price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;at_price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;
&lt;span class="c1"&gt;-- ... one JOIN per attribute&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_type_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Use Flat Catalog for Grids
&lt;/h3&gt;

&lt;p&gt;Enable flat catalog tables to replace EAV joins with single-table lookups:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ Flat catalog is deprecated in Magento 2.4.6+ for frontend use, but &lt;strong&gt;still works and is safe for admin grids&lt;/strong&gt;. Adobe removed frontend flat support but the admin grid data provider still falls back to flat tables when available.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Custom Flat Grid Tables
&lt;/h3&gt;

&lt;p&gt;For grids with millions of rows, create a dedicated flat table indexed specifically for filtering:&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales_order_grid_flat&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&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;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;increment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;g&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="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grand_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;billing_name&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;email&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_email&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_order_grid&lt;/span&gt; &lt;span class="k"&gt;g&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;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales_order_grid_flat&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&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;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_status_created&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;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_increment&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;increment_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="n"&gt;FULLTEXT&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_billing_name&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;billing_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point your grid's collection to this table instead of the normalized order grid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimize Mass Actions and Batch Operations
&lt;/h2&gt;

&lt;p&gt;Mass actions (hold, invoice, cancel, delete) on 200+ records are notorious for timeouts.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Increase Admin PHP Memory Limit
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .htaccess or php-fpm pool config for admin routes&lt;/span&gt;
&lt;span class="k"&gt;php_value&lt;/span&gt; &lt;span class="s"&gt;memory_limit&lt;/span&gt; &lt;span class="mi"&gt;1024M&lt;/span&gt;
&lt;span class="s"&gt;php_value&lt;/span&gt; &lt;span class="s"&gt;max_execution_time&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or isolate admin to a separate PHP-FPM pool with higher limits:&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;^/admin/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_pass&lt;/span&gt; &lt;span class="s"&gt;admin_php:9000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_read_timeout&lt;/span&gt; &lt;span class="s"&gt;300s&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;
  
  
  2. Batch Mass Actions with Chunking
&lt;/h3&gt;

&lt;p&gt;Override the mass action controller to process in smaller batches:&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;// Process 50 orders at a time instead of all 200&lt;/span&gt;
&lt;span class="nv"&gt;$collection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCollection&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;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="nv"&gt;$collection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setPageSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$collection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getLastPageNumber&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$page&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nv"&gt;$pages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$collection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setCurPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$collection&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;orderManagement&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getEntityId&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$collection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;clear&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;
  
  
  Grid-Specific Caching Strategies
&lt;/h2&gt;

&lt;p&gt;Since admin grids bypass the full page cache, you need grid-level solutions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Collection Cache with Magento's Cache
&lt;/h3&gt;

&lt;p&gt;Cache the collection result for grids that don't change frequently (e.g., product attribute grid):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CachedProductGridCollection&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ProductCollection&lt;/span&gt;
&lt;span class="p"&gt;{&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;_beforeLoad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'admin_product_grid_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;md5&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;getSelect&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;__toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="nv"&gt;$cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cache&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="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="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;_beforeLoad&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;
  
  
  2. Database Query Cache (MySQL 8.0)
&lt;/h3&gt;

&lt;p&gt;Enable the query cache for read-heavy admin queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;query_cache_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;query_cache_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;268435456&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- 256MB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ MySQL 8.0 removed query_cache. For MySQL 8.0+, use ProxySQL query cache or application-level caching instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For MySQL 8.0, move query caching to the application layer or use &lt;strong&gt;ProxySQL&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ProxySQL query caching&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;mysql_query_rules&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;cache_ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;match_pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SELECT.*FROM sales_order_grid.*'&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;QUERY&lt;/span&gt; &lt;span class="n"&gt;RULES&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Remove Unused Grid Features
&lt;/h2&gt;

&lt;p&gt;Every feature in the grid adds cost. Audit what your team actually uses:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Performance Cost&lt;/th&gt;
&lt;th&gt;Safe to Disable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inline editing&lt;/td&gt;
&lt;td&gt;High (saves per cell)&lt;/td&gt;
&lt;td&gt;Usually yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drag-and-drop row reordering&lt;/td&gt;
&lt;td&gt;Medium (position recalculation)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thumbnail column&lt;/td&gt;
&lt;td&gt;High (image generation)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-text customer address search&lt;/td&gt;
&lt;td&gt;Very High (LIKE on large text)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export to CSV/XML/Excel&lt;/td&gt;
&lt;td&gt;Medium (serialize + file I/O)&lt;/td&gt;
&lt;td&gt;If not used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-select filters&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Usually safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time total count&lt;/td&gt;
&lt;td&gt;Medium (COUNT(*) on every load)&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;Disable inline editing:&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;listing&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;settings&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;editorConfig&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;enabled&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/enabled&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/editorConfig&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/settings&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/listing&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance Impact: Before and After
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grid&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After Optimizations&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sales orders (50k rows)&lt;/td&gt;
&lt;td&gt;8.2s&lt;/td&gt;
&lt;td&gt;0.9s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.1x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Products (200k SKUs)&lt;/td&gt;
&lt;td&gt;12.4s&lt;/td&gt;
&lt;td&gt;1.8s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.9x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customers (100k accounts)&lt;/td&gt;
&lt;td&gt;6.7s&lt;/td&gt;
&lt;td&gt;1.1s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.1x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invoices (30k records)&lt;/td&gt;
&lt;td&gt;5.3s&lt;/td&gt;
&lt;td&gt;0.7s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.6x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credit memos (10k records)&lt;/td&gt;
&lt;td&gt;4.1s&lt;/td&gt;
&lt;td&gt;0.6s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.8x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Optimizations applied: composite indexes, flat tables, page size reduction, column hiding, collection caching, disabled inline editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Go Further
&lt;/h2&gt;

&lt;p&gt;For stores with 1M+ orders or 500k+ SKUs, admin grids need architectural changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated read replica for admin queries&lt;/strong&gt; — split admin reads to a separate MySQL instance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elasticsearch-backed product grid&lt;/strong&gt; — replace MySQL product grid with ES for instant filtering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom microservice grid&lt;/strong&gt; — offload heavy grids to a Node.js/Python service with its own indexed store&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin-specific CDN&lt;/strong&gt; — cache rendered grid HTML for 30-60 seconds (acceptable for internal tools)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;Admin grid performance is a productivity multiplier. Small optimizations compound:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add composite indexes for your most-used grid filter combinations&lt;/li&gt;
&lt;li&gt;Reduce default &lt;code&gt;pageSize&lt;/code&gt; from 200 to 20-50&lt;/li&gt;
&lt;li&gt;Hide expensive columns (thumbnails, addresses) by default&lt;/li&gt;
&lt;li&gt;Enable flat catalog tables for product/customer grids&lt;/li&gt;
&lt;li&gt;Chunk mass actions to avoid PHP timeouts&lt;/li&gt;
&lt;li&gt;Cache collection results for stable, read-heavy grids&lt;/li&gt;
&lt;li&gt;Disable unused features (inline editing, drag-and-drop, real-time counts)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your operations team will thank you — and your server will too.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>performance</category>
      <category>backend</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 MySQL Read/Write Replication Splitting: Scale Your Database Horizontally</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Wed, 29 Jul 2026 09:12:50 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-mysql-readwrite-replication-splitting-scale-your-database-horizontally-39e1</link>
      <guid>https://dev.to/magevanta/magento-2-mysql-readwrite-replication-splitting-scale-your-database-horizontally-39e1</guid>
      <description>&lt;p&gt;When your Magento 2 store outgrows a single database server, the first wall you hit isn't CPU or memory — it's &lt;strong&gt;connection contention&lt;/strong&gt;. MySQL can only handle so many concurrent connections before query latency spikes, transactions queue up, and your store grinds to a crawl.&lt;/p&gt;

&lt;p&gt;The standard production fix is &lt;strong&gt;read/write replication splitting&lt;/strong&gt;: offload SELECT queries to replica databases while writes go to the primary. Magento 2 has built-in support for this, but most developers don't configure it correctly — or at all.&lt;/p&gt;

&lt;p&gt;In this guide, you'll learn exactly how to set up MySQL replication for Magento 2, configure the read/write connection split, handle replication lag, and monitor everything in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Replication Splitting Matters for Magento 2
&lt;/h2&gt;

&lt;p&gt;Magento 2 is a database-heavy application. Every page load does dozens — sometimes hundreds — of SELECT queries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend catalog browsing&lt;/strong&gt;: category pages, product pages, layered navigation — all read-heavy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkout&lt;/strong&gt;: reads customer data, quotes, shipping rates, payment info — mix of reads and writes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin operations&lt;/strong&gt;: grid loads, report generation, order management — predominantly reads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indexers&lt;/strong&gt;: massive batch reads and writes during reindex&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cron jobs&lt;/strong&gt;: log cleanup, cache warming, analytics aggregation — mixed workload&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a single database server, all these reads compete with writes for connections, locks, and I/O. The result: &lt;strong&gt;read queries block writes, and writes block reads&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With read/write splitting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SELECT queries&lt;/strong&gt; go to replicas with relaxed transaction isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INSERT/UPDATE/DELETE&lt;/strong&gt; go to the primary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read replicas scale horizontally&lt;/strong&gt; — add more as traffic grows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primary stays fast&lt;/strong&gt; because it's not serving read traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Magento 2's Read/Write Connection System Works
&lt;/h2&gt;

&lt;p&gt;Magento 2 uses Zend_Db under the hood (in &lt;code&gt;Magento\Framework\DB\Adapter\AdapterInterface&lt;/code&gt;). The connection configuration lives in &lt;code&gt;app/etc/env.php&lt;/code&gt; under the &lt;code&gt;db&lt;/code&gt; key.&lt;/p&gt;

&lt;p&gt;Here's the basic out-of-the-box config:&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 — single server&lt;/span&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;'indexer'&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;'***'&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;'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="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;'***'&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="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Magento has two connection pools by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;default&lt;/code&gt;&lt;/strong&gt; — used by most application code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;indexer&lt;/code&gt;&lt;/strong&gt; — used exclusively by indexers (can use a separate server)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Actual ResourceConnection Resolution
&lt;/h3&gt;

&lt;p&gt;Magento's &lt;code&gt;Magento\Framework\App\ResourceConnection&lt;/code&gt; uses a simple pattern: when you request connection &lt;code&gt;default&lt;/code&gt;, it checks if there's a &lt;code&gt;default_read&lt;/code&gt; connection configured. If yes, SELECT queries use that; if not, everything goes to &lt;code&gt;default&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This means your &lt;code&gt;env.php&lt;/code&gt; should look like:&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;'indexer'&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;'db-primary'&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;'***'&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;'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="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;'db-primary'&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;'***'&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="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'default_read'&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;'db-replica-1'&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;'***'&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="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;With this config, Magento automatically sends SELECT queries through &lt;code&gt;default_read&lt;/code&gt; (the replica) while writes go through &lt;code&gt;default&lt;/code&gt; (the primary).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Set Up MySQL Replication
&lt;/h2&gt;

&lt;p&gt;Before Magento can use a read replica, you need MySQL replication running. Here's the minimal production setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  On the Primary Server
&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/mysql/mariadb.conf.d/replication.cnf
&lt;/span&gt;&lt;span class="nn"&gt;[mysqld]&lt;/span&gt;
&lt;span class="py"&gt;server_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;log_bin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/mysql/mysql-bin.log&lt;/span&gt;
&lt;span class="py"&gt;binlog_format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ROW&lt;/span&gt;
&lt;span class="py"&gt;expire_logs_days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;7&lt;/span&gt;
&lt;span class="py"&gt;max_binlog_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1G&lt;/span&gt;
&lt;span class="py"&gt;binlog_do_db&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;magento&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a replication user:&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'replicator'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'strong-password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="n"&gt;REPLICATION&lt;/span&gt; &lt;span class="n"&gt;SLAVE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'replicator'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  On the Replica Server
&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/mysql/mariadb.conf.d/replication.cnf
&lt;/span&gt;&lt;span class="nn"&gt;[mysqld]&lt;/span&gt;
&lt;span class="py"&gt;server_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;2&lt;/span&gt;
&lt;span class="py"&gt;log_bin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/mysql/mysql-bin.log&lt;/span&gt;
&lt;span class="py"&gt;binlog_format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ROW&lt;/span&gt;
&lt;span class="py"&gt;expire_logs_days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;7&lt;/span&gt;
&lt;span class="py"&gt;relay_log&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/mysql/mysql-relay-bin.log&lt;/span&gt;
&lt;span class="py"&gt;read_only&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;read_only = 1&lt;/code&gt; is critical — it prevents any direct writes to the replica.&lt;/p&gt;

&lt;p&gt;Initialise replication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;CHANGE&lt;/span&gt; &lt;span class="n"&gt;MASTER&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt;
  &lt;span class="n"&gt;MASTER_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'db-primary'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;MASTER_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'replicator'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;MASTER_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'strong-password'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;MASTER_LOG_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'mysql-bin.000001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;MASTER_LOG_POS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;START&lt;/span&gt; &lt;span class="n"&gt;SLAVE&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;SLAVE&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="k"&gt;G&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Sync the Existing Data
&lt;/h3&gt;

&lt;p&gt;If you already have a live Magento database, use &lt;code&gt;mysqldump&lt;/code&gt; to seed the replica:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysqldump &lt;span class="nt"&gt;--single-transaction&lt;/span&gt; &lt;span class="nt"&gt;--quick&lt;/span&gt; &lt;span class="nt"&gt;--routines&lt;/span&gt; &lt;span class="nt"&gt;--triggers&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;db-primary magento &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; magento_dump.sql

mysql &lt;span class="nt"&gt;--host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;db-replica-1 magento &amp;lt; magento_dump.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--single-transaction&lt;/code&gt; flag gives you a consistent snapshot without locking all tables — crucial for a production primary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Configure Magento for Read/Write Splitting
&lt;/h2&gt;

&lt;p&gt;Once replication is running, 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="c1"&gt;// app/etc/env.php&lt;/span&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;'db-primary'&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;'***'&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;'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;'driver_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="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MYSQL_ATTR_INIT_COMMAND&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="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'default_read'&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;'db-replica-1'&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;'***'&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;'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;'driver_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="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MYSQL_ATTR_INIT_COMMAND&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="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'indexer'&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;'db-primary'&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;'***'&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="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'slave_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_read'&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;
  
  
  Multiple Read Replicas
&lt;/h3&gt;

&lt;p&gt;Need more read capacity? Magento 2 supports multiple read connections. Add them as separate entries:&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;'default_read'&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;'db-replica-1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="s1"&gt;'default_read_2'&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;'db-replica-2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="s1"&gt;'slave_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_read'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'default_read_2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Magento's &lt;code&gt;ResourceConnection&lt;/code&gt; picks one randomly from the available read connections — simple round-robin load balancing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify It's Working
&lt;/h3&gt;

&lt;p&gt;Run this from 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;bin/magento setup:db:status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check which server queries hit by inspecting the general log on your replicas, or by adding a temporary connection log:&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;$resource&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\App\ResourceConnection&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;$readConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$resource&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="s1"&gt;'default_read'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'Read server: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$readConnection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetchOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT @@hostname;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$writeConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$resource&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="s1"&gt;'default'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'Write server: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$writeConnection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetchOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT @@hostname;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Handle Replication Lag
&lt;/h2&gt;

&lt;p&gt;Replication lag is the #1 challenge with read/write splitting. If a customer places an order and the next page load reads from a lagging replica, their order won't appear — which is a terrible experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  The After-Write Read Pattern
&lt;/h3&gt;

&lt;p&gt;In checkout and customer account areas, force reads to the primary when consistency matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;aroundGetCheckoutData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;\Magento\Checkout\Model\Session&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;callable&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;session&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getLastOrderId&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;resourceConnection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setUseWriteConnection&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$proceed&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;
  
  
  Set Max Acceptable Lag
&lt;/h3&gt;

&lt;p&gt;Use MySQL's &lt;code&gt;Seconds_Behind_Master&lt;/code&gt; metric. Build a health-check script that swaps connections when lag exceeds a threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;LAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mysql &lt;span class="nt"&gt;-h&lt;/span&gt; db-replica-1 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SHOW SLAVE STATUS&lt;/span&gt;&lt;span class="se"&gt;\G&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Seconds_Behind_Master"&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;$LAG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 30 &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;php bin/magento config:set system/db/read_host db-primary
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this as a cron job every minute for best results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: ProxySQL — The Production-Grade Alternative
&lt;/h2&gt;

&lt;p&gt;Configuring read/write splitting in &lt;code&gt;env.php&lt;/code&gt; works, but it has limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No connection pooling&lt;/strong&gt; — each PHP-FPM worker opens its own connection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lag-aware routing&lt;/strong&gt; — Magento doesn't know if a replica is behind&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No query-level routing&lt;/strong&gt; — all SELECTs go to replicas, even transactional ones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;ProxySQL&lt;/strong&gt; solves all of these. It sits between Magento and MySQL as a transparent proxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Magento → ProxySQL (6033) → Primary
                         → Replica 1
                         → Replica 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install ProxySQL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;proxysql
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;proxysql
systemctl start proxysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure Query Rules
&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;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;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'db-primary'&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;-- writes&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;'db-replica-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;-- reads&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;'db-replica-2'&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;-- reads&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;'***'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;mysql_query_rules&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rule_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match_pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination_hostgroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apply&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'^SELECT .*'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'^INSERT.*'&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="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&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;'^UPDATE.*'&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="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&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;'^DELETE.*'&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="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="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="k"&gt;LOAD&lt;/span&gt; &lt;span class="n"&gt;MYSQL&lt;/span&gt; &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;RULES&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="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="n"&gt;SAVE&lt;/span&gt; &lt;span class="n"&gt;MYSQL&lt;/span&gt; &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;RULES&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;p&gt;Then update &lt;code&gt;env.php&lt;/code&gt; to point at ProxySQL:&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;'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;'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="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ProxySQL handles &lt;strong&gt;connection pooling&lt;/strong&gt; (reusing connections between PHP-FPM workers) and &lt;strong&gt;query-level routing&lt;/strong&gt;, which is far more granular than connection-level splitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What NOT to Route to Replicas
&lt;/h2&gt;

&lt;p&gt;These operations must always hit the primary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Checkout quote reads&lt;/td&gt;
&lt;td&gt;Must see the latest quote state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer session reads&lt;/td&gt;
&lt;td&gt;Must reflect recent login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin ACL checks&lt;/td&gt;
&lt;td&gt;Must see permission changes immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache tag invalidation&lt;/td&gt;
&lt;td&gt;Race conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries inside transactions&lt;/td&gt;
&lt;td&gt;Transaction scope consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Magento's &lt;code&gt;ResourceConnection&lt;/code&gt; automatically routes queries inside a transaction to the write connection. For explicit control in checkout or admin code:&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;$resource&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="s1"&gt;'checkout'&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;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT ...'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;You need visibility into three things:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Replication Lag
&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;SLAVE&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="k"&gt;G&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set alerts at 10 seconds (warning) and 30 seconds (critical).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Read/Write Ratio
&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="k"&gt;GLOBAL&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;'Com_select'&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;GLOBAL&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;'Com_insert'&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;GLOBAL&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;'Com_update'&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;GLOBAL&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;'Com_delete'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a typical Magento store, you'll see 80-90% reads, 10-20% writes. If your read ratio is below 70%, you might not benefit much from read replicas.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Connection Distribution (ProxySQL)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;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;h2&gt;
  
  
  Performance Impact: What to Expect
&lt;/h2&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;Single DB&lt;/th&gt;
&lt;th&gt;Read Replica&lt;/th&gt;
&lt;th&gt;ProxySQL + 2 Replicas&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catalog page load (p50)&lt;/td&gt;
&lt;td&gt;420ms&lt;/td&gt;
&lt;td&gt;310ms&lt;/td&gt;
&lt;td&gt;260ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout submit (p95)&lt;/td&gt;
&lt;td&gt;1.8s&lt;/td&gt;
&lt;td&gt;1.6s&lt;/td&gt;
&lt;td&gt;1.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max concurrent requests&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;450&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary CPU load (peaks)&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;45%&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest win isn't latency — it's &lt;strong&gt;capacity&lt;/strong&gt;. Offloading reads frees the primary to handle writes faster, and your store can handle 2-4x more traffic before hitting a bottleneck.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  ❌ Using the Same Server for Read and Write
&lt;/h3&gt;

&lt;p&gt;If both &lt;code&gt;default&lt;/code&gt; and &lt;code&gt;default_read&lt;/code&gt; point to the same host, you're adding overhead with zero benefit. Use separate servers.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Skipping read_only on Replicas
&lt;/h3&gt;

&lt;p&gt;Set &lt;code&gt;read_only = 1&lt;/code&gt; in MySQL config on all replicas. If Magento accidentally sends a write, &lt;code&gt;read_only&lt;/code&gt; prevents data corruption.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Not Warming the Replica Buffer Pool
&lt;/h3&gt;

&lt;p&gt;A fresh replica needs to warm its buffer pool. Run full scans on your largest tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;table &lt;span class="k"&gt;in &lt;/span&gt;catalog_product_entity catalog_category_product &lt;span class="se"&gt;\&lt;/span&gt;
             sales_order sales_order_item quote quote_item&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;mysql &lt;span class="nt"&gt;-h&lt;/span&gt; db-replica-1 magento &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT COUNT(*) FROM &lt;/span&gt;&lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="s2"&gt;;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, the first 24 hours of queries will be slow.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Ignoring Long-Running Queries
&lt;/h3&gt;

&lt;p&gt;Replicas share the same schema as the primary. If a slow query causes issues on the primary, it will also cause issues on the replica. Profile both servers with &lt;code&gt;pt-query-digest&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to NOT Use Read/Write Splitting
&lt;/h2&gt;

&lt;p&gt;Read/write splitting adds operational complexity. Skip it if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your store handles fewer than 50,000 monthly visitors&lt;/li&gt;
&lt;li&gt;Your MySQL server runs below 60% CPU during peak&lt;/li&gt;
&lt;li&gt;Average page loads are under 400ms on a single server&lt;/li&gt;
&lt;li&gt;You don't have DevOps support for the infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For smaller stores, invest in &lt;strong&gt;caching&lt;/strong&gt; (Redis, Varnish) and &lt;strong&gt;query optimization&lt;/strong&gt; first — bigger wins, less operational overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;Magento 2's built-in read/write connection support makes horizontal database scaling accessible:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set up MySQL replication with &lt;code&gt;binlog_format = ROW&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;default_read&lt;/code&gt; connection in &lt;code&gt;env.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Handle replication lag with after-write reads and lag monitoring&lt;/li&gt;
&lt;li&gt;For production scale, use ProxySQL for query-level split and connection pooling&lt;/li&gt;
&lt;li&gt;Monitor &lt;code&gt;Seconds_Behind_Master&lt;/code&gt; and connection distribution&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The ceiling on Magento's database performance isn't the application — it's the architecture. Read/write replication splitting removes the single-server bottleneck and gives your store room to grow through peak traffic, flash sales, and catalog expansion without a full replatform.&lt;/p&gt;

</description>
      <category>magento2</category>
      <category>performance</category>
      <category>mysql</category>
      <category>database</category>
    </item>
    <item>
      <title>Magento 2 Database Table Partitioning &amp; Archiving Strategies</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Wed, 29 Jul 2026 09:10:01 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-database-table-partitioning-archiving-strategies-3n3k</link>
      <guid>https://dev.to/magevanta/magento-2-database-table-partitioning-archiving-strategies-3n3k</guid>
      <description>&lt;p&gt;Every Magento 2 store eventually hits the same wall: the database keeps growing, queries keep slowing down, and nobody notices until checkout times spike or admin grids become unusable. The &lt;code&gt;sales_order&lt;/code&gt; table has 5 million rows. The &lt;code&gt;quote&lt;/code&gt; table has 12 million. The &lt;code&gt;log_visitor&lt;/code&gt; table doesn't even bear thinking about.&lt;/p&gt;

&lt;p&gt;This guide covers practical strategies for partitioning, archiving, and maintaining Magento's largest tables — so your store stays fast without losing historical data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Tables Cause the Most Pain?
&lt;/h2&gt;

&lt;p&gt;Not all tables grow equally. These are the usual suspects:&lt;/p&gt;

&lt;h3&gt;
  
  
  Transactional Tables
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;quote&lt;/code&gt; / &lt;code&gt;quote_item&lt;/code&gt;&lt;/strong&gt; — Every cart session creates rows. Guest carts never clean up by default. At 10k daily visitors with 70% abandonment, you're adding ~7,000 abandoned quotes per day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sales_order&lt;/code&gt; / &lt;code&gt;sales_order_item&lt;/code&gt;&lt;/strong&gt; — Grows linearly with sales. A store doing 500 orders/day hits 1.8M rows/year in &lt;code&gt;sales_order&lt;/code&gt; alone, plus 3-5x that in &lt;code&gt;sales_order_item&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sales_invoice&lt;/code&gt; / &lt;code&gt;sales_invoice_item&lt;/code&gt; / &lt;code&gt;sales_creditmemo&lt;/code&gt; / &lt;code&gt;sales_creditmemo_item&lt;/code&gt;&lt;/strong&gt; — Same growth pattern, sometimes with additional rows per order.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Log Tables
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;log_visitor&lt;/code&gt; / &lt;code&gt;log_visitor_info&lt;/code&gt; / &lt;code&gt;log_url&lt;/code&gt; / &lt;code&gt;log_url_info&lt;/code&gt;&lt;/strong&gt; — Magento's built-in visitor logging. Creates multiple rows per page view. On a busy store, these tables can grow by 100k+ rows per day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;report_event&lt;/code&gt; / &lt;code&gt;report_viewed_product_index&lt;/code&gt;&lt;/strong&gt; — Reporting tables that accumulate indefinitely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Search Tables
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;search_query&lt;/code&gt;&lt;/strong&gt; — Every unique search term gets logged. Popular stores accumulate hundreds of thousands of rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;catalogsearch_fulltext&lt;/code&gt;&lt;/strong&gt; — The full-text search index can become massive with large catalogs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Strategy 1: Clean Up What You Don't Need
&lt;/h2&gt;

&lt;p&gt;Before partitioning anything, remove data you'll never use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Purge Old Visitor Logs
&lt;/h3&gt;

&lt;p&gt;Magento's visitor logs are rarely useful after 30 days. Add a cron job or manual cleanup:&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;-- Remove visitor data older than 30 days&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;log_visitor&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;visit_last_activity_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;log_visitor_info&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;visitor_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visitor_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;log_visitor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;log_url&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;visitor_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;visitor_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;log_visitor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;log_url_info&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;url_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;url_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;log_url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Always use &lt;code&gt;LIMIT&lt;/code&gt; with deletes on large tables. A single &lt;code&gt;DELETE&lt;/code&gt; on 10M rows locks the table for minutes and fills the InnoDB buffer pool with undo data. Run the delete in a loop or use a batched approach.&lt;/p&gt;

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

&lt;p&gt;Guest quotes accumulate fast. Magento has a built-in cleanup, but it's often not aggressive enough:&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;-- Remove guest quotes older than 7 days (they're never coming back)&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qi&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;quote_item&lt;/span&gt; &lt;span class="n"&gt;qi&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;qi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quote_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Magento's &lt;code&gt;sales/cleanup/quotes&lt;/code&gt; cron handles this, but check its configuration in &lt;code&gt;Stores &amp;gt; Configuration &amp;gt; Sales &amp;gt; Checkout &amp;gt; Shopping Cart&lt;/code&gt;. Set &lt;strong&gt;"Quote Lifetime (days)"&lt;/strong&gt; to something reasonable like 7-14 days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Truncate Search Query Table
&lt;/h3&gt;

&lt;p&gt;If you're not using search analytics, truncate it:&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;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;search_query&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 need some history, keep the last 90 days:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;search_query&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Strategy 2: Archive Old Orders
&lt;/h2&gt;

&lt;p&gt;Archiving moves completed orders from the main tables to archive tables, keeping the active tables small and fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Magento's Built-In Archive
&lt;/h3&gt;

&lt;p&gt;Magento provides order archiving through the admin: &lt;strong&gt;Sales &amp;gt; Orders &amp;gt; Archive&lt;/strong&gt;. You can also trigger it 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 sales:order:archive &lt;span class="nt"&gt;--date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2025-12-31
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This moves orders placed before the given date to archive tables (&lt;code&gt;sales_order_archive&lt;/code&gt;, etc.). The archive tables have identical structure, so reports and lookups still work — they just query a different table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom Archiving with Partition Swap
&lt;/h3&gt;

&lt;p&gt;For more control, create your own archive tables and use partition swapping:&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;-- 1. Create archive table matching structure&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales_order_archive&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="n"&gt;sales_order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Move old orders in batches&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;sales_order_archive&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_order&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2025-01-01'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Delete from main table (same batch)&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_order&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2025-01-01'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why batch by &lt;code&gt;entity_id&lt;/code&gt;?&lt;/strong&gt; A single &lt;code&gt;DELETE&lt;/code&gt; with a date filter on a 10M row table creates a massive transaction. Batching by primary key range keeps each transaction small and avoids long-running locks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reindex After Archiving
&lt;/h3&gt;

&lt;p&gt;After removing large amounts of data, rebuild affected indexers:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Archived orders still need index entries if you're displaying historical data, but the main order tables will reindex much faster with fewer rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 3: Table Partitioning
&lt;/h2&gt;

&lt;p&gt;MySQL partitioning splits a single logical table into multiple physical segments. Queries that include the partition key only scan relevant partitions, dramatically improving performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Range Partitioning by Date
&lt;/h3&gt;

&lt;p&gt;The most effective partitioning strategy for Magento's order tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales_order&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;RANGE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;YEAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;MONTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;p202401&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;LESS&lt;/span&gt; &lt;span class="k"&gt;THAN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;202402&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;p202402&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;LESS&lt;/span&gt; &lt;span class="k"&gt;THAN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;202403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;p202403&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;LESS&lt;/span&gt; &lt;span class="k"&gt;THAN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;202404&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;-- ... continue for each month&lt;/span&gt;
    &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;p202607&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;LESS&lt;/span&gt; &lt;span class="k"&gt;THAN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;202608&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;p_future&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;LESS&lt;/span&gt; &lt;span class="k"&gt;THAN&lt;/span&gt; &lt;span class="k"&gt;MAXVALUE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key rule:&lt;/strong&gt; The partition key must be part of every unique index. Since &lt;code&gt;sales_order&lt;/code&gt; has a primary key on &lt;code&gt;entity_id&lt;/code&gt;, you need to drop and recreate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales_order&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&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 why partitioning is easier to set up when you create the table from scratch — retrofitting on production requires downtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hash Partitioning for Quote Tables
&lt;/h3&gt;

&lt;p&gt;Quotes don't have a natural date range that queries use. Hash partitioning by &lt;code&gt;customer_id&lt;/code&gt; or &lt;code&gt;entity_id&lt;/code&gt; distributes data evenly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;HASH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PARTITIONS&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This improves concurrent access patterns — different customer sessions hit different partitions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partition Maintenance
&lt;/h3&gt;

&lt;p&gt;Add new partitions monthly and drop old ones:&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;-- Add next month&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales_order&lt;/span&gt; &lt;span class="n"&gt;REORGANIZE&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;p_future&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;p202608&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;LESS&lt;/span&gt; &lt;span class="k"&gt;THAN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;202609&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;p_future&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;LESS&lt;/span&gt; &lt;span class="k"&gt;THAN&lt;/span&gt; &lt;span class="k"&gt;MAXVALUE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Drop archived month&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales_order&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;p202401&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automate this with a monthly cron job. Dropping a partition is instant — it doesn't generate undo logs or lock the table.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Magento Queries Benefit?
&lt;/h3&gt;

&lt;p&gt;Not all queries automatically use partitions. The query must include the partition key in its &lt;code&gt;WHERE&lt;/code&gt; clause. Magento's order grid queries typically filter by date, so they benefit directly:&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;-- This query hits only 1 partition instead of scanning the whole table&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_order&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-08-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But queries that filter only by &lt;code&gt;entity_id&lt;/code&gt; or &lt;code&gt;increment_id&lt;/code&gt; will still scan all partitions. For those, add a covering index that includes &lt;code&gt;created_at&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 4: External Archiving for Reporting
&lt;/h2&gt;

&lt;p&gt;If you need years of order history for reporting but don't want it polluting the production database, export it:&lt;/p&gt;

&lt;h3&gt;
  
  
  Export to a Reporting Database
&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;# Dump old orders to a separate database&lt;/span&gt;
mysqldump &lt;span class="nt"&gt;--where&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"created_at &amp;lt; '2025-01-01'"&lt;/span&gt; magento sales_order sales_order_item | &lt;span class="se"&gt;\&lt;/span&gt;
mysql magento_archive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then truncate or partition the production tables. Your analytics team queries the archive database; your production database stays lean.&lt;/p&gt;

&lt;h3&gt;
  
  
  Elasticsearch for Historical Search
&lt;/h3&gt;

&lt;p&gt;If customers need to search their order history, index historical orders in Elasticsearch. Magento's search infrastructure can query ES much faster than scanning a massive &lt;code&gt;sales_order&lt;/code&gt; table for a customer's old orders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring Table Growth
&lt;/h2&gt;

&lt;p&gt;Set up alerts before tables become a problem:&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 sizes&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;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="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;data_mb&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;index_length&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;index_mb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;table_rows&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;'magento'&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;'sales_order'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sales_order_item'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'log_visitor'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'log_url'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'search_query'&lt;/span&gt;&lt;span class="p"&gt;)&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;data_length&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;Set a cron to run this daily and alert you when any table exceeds a threshold (e.g., 1GB data or 5M rows).&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Cleanup Schedule
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;Retention&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log_visitor*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;Batch delete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log_url*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;Batch delete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;quote&lt;/code&gt; (guest)&lt;/td&gt;
&lt;td&gt;7 days&lt;/td&gt;
&lt;td&gt;Magento config + batch delete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;quote&lt;/code&gt; (customer)&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;Magento config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search_query&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;Batch delete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;report_event&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;Batch delete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sales_order&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2+ years&lt;/td&gt;
&lt;td&gt;Archive + partition drop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;A Magento database that isn't maintained will eventually bring your store to its knees. The combination of regular cleanup, partitioning for large tables, and archiving for historical data keeps query times low, indexers fast, and admin grids responsive.&lt;/p&gt;

&lt;p&gt;Don't wait until checkout takes 10 seconds. Set up monitoring, implement a cleanup schedule, and partition your largest tables before they become an emergency.&lt;/p&gt;

&lt;p&gt;The upfront investment is a few hours of setup and a monthly cron job. The payoff is a store that stays fast regardless of how many orders you've processed or how many abandoned carts are sitting in your database.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>mysql</category>
      <category>database</category>
      <category>performance</category>
    </item>
    <item>
      <title>Magento 2 CDN Setup: Optimize Static Content Delivery</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Tue, 28 Jul 2026 09:07:08 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-cdn-setup-optimize-static-content-delivery-26nc</link>
      <guid>https://dev.to/magevanta/magento-2-cdn-setup-optimize-static-content-delivery-26nc</guid>
      <description>&lt;p&gt;Content Delivery Networks (CDNs) are one of the most impactful yet underutilized performance tools in the Magento 2 ecosystem. While most store owners obsess over backend optimizations — database tuning, indexer speed, PHP-FPM settings — the frontend delivery of static assets often remains a blind spot. This is a mistake. A well-configured CDN can reduce Time to First Byte (TTFB), cut bandwidth costs, and dramatically improve the user experience for global audiences.&lt;/p&gt;

&lt;p&gt;In this guide, we'll walk through the complete CDN setup for Magento 2, covering static content, media files, and user-generated content. We'll also discuss advanced configuration for cache invalidation and HTTPS/TLS optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CDN Matters for Magento 2
&lt;/h2&gt;

&lt;p&gt;Magento 2 stores are asset-heavy. A typical product page loads dozens of JavaScript files, CSS bundles, product images, category thumbnails, and static theme assets. Without a CDN, every request hits your origin server, creating unnecessary load and increasing latency — especially for users geographically distant from your hosting location.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key benefits of using a CDN with Magento 2:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reduced latency&lt;/strong&gt;: Edge servers deliver content from locations closer to the user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin offload&lt;/strong&gt;: Static assets never hit your Magento server, freeing PHP-FPM workers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bandwidth savings&lt;/strong&gt;: Origin egress drops significantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved Core Web Vitals&lt;/strong&gt;: Faster LCP and reduced TTFB directly impact SEO&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DDoS resilience&lt;/strong&gt;: CDN edge absorbs traffic spikes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Should Go Through the CDN?
&lt;/h2&gt;

&lt;p&gt;A typical Magento 2 CDN setup handles three asset categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static Content&lt;/strong&gt; (&lt;code&gt;/static/&lt;/code&gt;) — JS, CSS, theme assets, font files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media&lt;/strong&gt; (&lt;code&gt;/media/&lt;/code&gt;) — Product images, category images, WYSIWYG uploads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Content&lt;/strong&gt; (&lt;code&gt;/pub/media/&lt;/code&gt;) — Customer uploads, order PDFs, invoices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Do NOT CDN these paths:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/rest/&lt;/code&gt; — API endpoints, dynamic&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/checkout/&lt;/code&gt; — Cart operations, dynamic&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/customer/&lt;/code&gt; — Account pages, session-based&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/admin/&lt;/code&gt; — Security risk&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Base URL Configuration
&lt;/h2&gt;

&lt;p&gt;Magento 2 supports separate base URLs for static and media content. Configure these in &lt;strong&gt;Stores &amp;gt; Configuration &amp;gt; Web &amp;gt; Base URLs&lt;/strong&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;Base&lt;/span&gt; &lt;span class="n"&gt;URL&lt;/span&gt;: &lt;span class="n"&gt;https&lt;/span&gt;://&lt;span class="n"&gt;magevanta&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;/
&lt;span class="n"&gt;Base&lt;/span&gt; &lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="n"&gt;Static&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt; &lt;span class="n"&gt;Files&lt;/span&gt;: &lt;span class="n"&gt;https&lt;/span&gt;://&lt;span class="n"&gt;cdn&lt;/span&gt;.&lt;span class="n"&gt;magevanta&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;/&lt;span class="n"&gt;static&lt;/span&gt;/
&lt;span class="n"&gt;Base&lt;/span&gt; &lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;Media&lt;/span&gt; &lt;span class="n"&gt;Files&lt;/span&gt;: &lt;span class="n"&gt;https&lt;/span&gt;://&lt;span class="n"&gt;cdn&lt;/span&gt;.&lt;span class="n"&gt;magevanta&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;/&lt;span class="n"&gt;media&lt;/span&gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For multi-store setups, configure per-store base URLs under the store scope. Always use HTTPS for CDN URLs — modern browsers flag mixed-content warnings, and HTTP/2 requires TLS.&lt;/p&gt;

&lt;p&gt;After changing base URLs, run a full cache flush:&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;h2&gt;
  
  
  Step 2: Static Content Deploy with Versioning
&lt;/h2&gt;

&lt;p&gt;Magento's static content deployment (&lt;code&gt;setup:static-content:deploy&lt;/code&gt;) generates versioned directories under &lt;code&gt;pub/static/&lt;/code&gt;. The version hash changes with each deployment, making CDN cache invalidation automatic — the URL changes, so the CDN fetches fresh content.&lt;/p&gt;

&lt;p&gt;Verify versioning 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:show dev/static/sign
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Should return &lt;code&gt;1&lt;/code&gt;. If not, enable it:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;With signing enabled, your static URLs look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://cdn.magevanta.com/static/version1234567890/frontend/Magento/luma/en_US/css/styles.css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you deploy new static content, the version hash updates and the CDN automatically serves the new files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Cloudflare Configuration
&lt;/h2&gt;

&lt;p&gt;Cloudflare is the most popular CDN choice for Magento stores due to its generous free tier and edge caching capabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Page Rules
&lt;/h3&gt;

&lt;p&gt;Create these page rules in order (Cloudflare evaluates top-down):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;*cdn.magevanta.com/static/*&lt;/code&gt; → Cache Level: Cache Everything, Edge TTL: 1 month&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*cdn.magevanta.com/media/*&lt;/code&gt; → Cache Level: Cache Everything, Edge TTL: 7 days&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*magevanta.com/checkout/*&lt;/code&gt; → Cache Level: Bypass&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*magevanta.com/customer/*&lt;/code&gt; → Cache Level: Bypass&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Origin Configuration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSL/TLS mode&lt;/strong&gt;: Full (strict) — requires valid certificate on origin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always Use HTTPS&lt;/strong&gt;: On&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic HTTPS Rewrites&lt;/strong&gt;: On&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brotli compression&lt;/strong&gt;: On — better than gzip, roughly 15-20% smaller&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP/2&lt;/strong&gt;: Enabled by default&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP/3 (QUIC)&lt;/strong&gt;: Enable for reduced latency on mobile&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cache Headers
&lt;/h3&gt;

&lt;p&gt;Ensure your origin sends proper cache headers for static assets. Add to your Nginx configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)&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;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;nosniff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;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="s"&gt;must-revalidate"&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;immutable&lt;/code&gt; directive tells browsers the file will never change — the version hash handles updates — allowing aggressive browser caching without stale content risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Image Optimization
&lt;/h2&gt;

&lt;p&gt;Most CDNs offer automatic image optimization. For Cloudflare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polish&lt;/strong&gt;: Enable Lossy for product images, Lossless for logos and icons&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebP&lt;/strong&gt;: Automatic conversion for supporting browsers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mirage&lt;/strong&gt;: Mobile image optimization that resizes for viewport&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Magento stores with thousands of product images, this alone can reduce image payload by 30-50%.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nginx-Level WebP Fallback
&lt;/h3&gt;

&lt;p&gt;If your CDN doesn't handle WebP conversion, serve it directly via Nginx with the &lt;code&gt;ngx_http_image_filter_module&lt;/code&gt; or pre-generate WebP variants during &lt;code&gt;setup:static-content:deploy&lt;/code&gt; using a custom deploy strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Cache Invalidation Strategy
&lt;/h2&gt;

&lt;p&gt;Static content is easy — version hashes handle it. Media content requires explicit invalidation when products are updated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: Cloudflare API
&lt;/h3&gt;

&lt;p&gt;After product import or mass update, purge specific paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PURGE_URLS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'["https://cdn.magevanta.com/media/catalog/product/cache/*"]'&lt;/span&gt;

curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.cloudflare.com/client/v4/zones/&lt;/span&gt;&lt;span class="nv"&gt;$ZONE_ID&lt;/span&gt;&lt;span class="s2"&gt;/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 &lt;/span&gt;&lt;span class="nv"&gt;$API_TOKEN&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;"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="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;files&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$PURGE_URLS&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option B: Magento Cache Tags
&lt;/h3&gt;

&lt;p&gt;Advanced setups can integrate CDN invalidation with Magento's cache tags. When Magento purges FPC for a product, trigger a CDN purge for that product's media paths. This requires custom module development using the &lt;code&gt;clean_cache_by_tags&lt;/code&gt; event.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option C: Stale-While-Revalidate
&lt;/h3&gt;

&lt;p&gt;For high-traffic stores, consider &lt;code&gt;stale-while-revalidate&lt;/code&gt; headers. The CDN serves the cached version immediately while fetching a fresh copy in the background. This eliminates cache-miss latency spikes during invalidation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: public, max-age=86400, stale-while-revalidate=3600
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Multi-Region CDN with Origin Shield
&lt;/h2&gt;

&lt;p&gt;If your store serves multiple regions, enable Cloudflare's &lt;strong&gt;Origin Shield&lt;/strong&gt; (or equivalent in other CDNs). Without it, each regional edge node independently fetches from origin on cache miss. With Origin Shield, a single intermediate cache layer absorbs misses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Without shield: 200 edge nodes × 1% miss rate = 2 origin requests per asset&lt;/li&gt;
&lt;li&gt;With shield: 200 edge nodes → 1 shield node → 1 origin request per asset&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For stores running on limited origin infrastructure, this can prevent cache stampedes during product launches or sale events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Monitoring and Verification
&lt;/h2&gt;

&lt;p&gt;After CDN deployment, verify configuration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Response headers&lt;/strong&gt;: Check &lt;code&gt;CF-Cache-Status&lt;/code&gt; header:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;HIT&lt;/code&gt; — served from CDN edge&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MISS&lt;/code&gt; — fetched from origin (first request)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DYNAMIC&lt;/code&gt; — not cached, check page rules&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Waterfall analysis&lt;/strong&gt;: Use Chrome DevTools or WebPageTest to confirm static assets load from CDN domain and show cache hits.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Origin bandwidth&lt;/strong&gt;: Monitor your hosting panel — successful CDN offload drops static content bandwidth to near zero.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache hit ratio&lt;/strong&gt;: Aim for &amp;gt;95% hit ratio on static assets. Below that, review your page rules and cache headers.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;strong&gt;Mixed content warnings&lt;/strong&gt;: If your base URL is HTTP but the page loads over HTTPS, browsers block assets. Always use HTTPS CDN URLs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CORS issues for fonts&lt;/strong&gt;: WOFF/2 font files served from a different domain need CORS headers:&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;.(woff|woff2)&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;Access-Control-Allow-Origin&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cookie forwarding&lt;/strong&gt;: Ensure the CDN strips or doesn't forward cookies for static asset requests. Cookies prevent caching and expose session data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin panel exposure&lt;/strong&gt;: Never CDN admin URLs. Restrict admin access to origin IP whitelist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale assets after deploy&lt;/strong&gt;: If you're not using versioned URLs, users may see old CSS/JS after a deploy. Always enable &lt;code&gt;dev/static/sign&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Impact
&lt;/h2&gt;

&lt;p&gt;A properly configured CDN typically delivers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;30-50% reduction&lt;/strong&gt; in page load time for global users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;60-80% reduction&lt;/strong&gt; in origin bandwidth usage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LCP improvement&lt;/strong&gt; of 0.5-1.5 seconds on image-heavy pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTFB reduction&lt;/strong&gt; from 200-500ms to 20-50ms for cached assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a store doing $1M+ revenue, this directly translates to higher conversion rates — Amazon famously estimated every 100ms delay costs 1% in revenue.&lt;/p&gt;

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

&lt;p&gt;CDN configuration is low-hanging fruit for Magento 2 performance. The setup is straightforward — base URL changes, Cloudflare page rules, and proper cache headers — yet the impact is outsized. For stores with international customers or heavy static asset loads, a CDN moves from "nice to have" to essential infrastructure.&lt;/p&gt;

&lt;p&gt;Start with static and media content, monitor your cache hit ratio, then expand to advanced features like image optimization and automatic purge integration. Your origin server — and your users — will thank you.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>performance</category>
      <category>cdn</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Magento 2 Database Index Strategy &amp; Query Optimization</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Mon, 27 Jul 2026 09:08:14 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-database-index-strategy-query-optimization-2174</link>
      <guid>https://dev.to/magevanta/magento-2-database-index-strategy-query-optimization-2174</guid>
      <description>&lt;p&gt;Magento 2's database is its backbone. Every product load, category browse, cart update, and checkout step hits MySQL. When your catalog grows past 50K SKUs and traffic climbs, poorly optimized queries become the silent killer — causing slow page loads, checkout timeouts, and frustrated customers.&lt;/p&gt;

&lt;p&gt;Most Magento developers know about Redis caching and Varnish, but few dig into the database layer where real performance gains hide. This guide covers practical MySQL index strategies and query optimization techniques tailored specifically for Magento 2's architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Magento 2's Database Schema
&lt;/h2&gt;

&lt;p&gt;Magento 2 uses a hybrid EAV (Entity-Attribute-Value) and flat table model. Products, customers, and categories store their core data in EAV tables, while indexed data lives in flat tables for faster retrieval.&lt;/p&gt;

&lt;h3&gt;
  
  
  The EAV Table Structure
&lt;/h3&gt;

&lt;p&gt;A single product's name doesn't live in one row. Instead, it's spread across multiple tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;catalog_product_entity              → entity_id, sku, type_id
catalog_product_entity_varchar      → entity_id, attribute_id, store_id, value
catalog_product_entity_int          → entity_id, attribute_id, store_id, value
catalog_product_entity_decimal      → entity_id, attribute_id, store_id, value
catalog_product_entity_text         → entity_id, attribute_id, store_id, value
catalog_product_entity_datetime     → entity_id, attribute_id, store_id, value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loading a single product with 20 attributes means joining 5-6 EAV tables. Each join multiplies the query cost. At 100K products, this becomes brutal without proper indexing.&lt;/p&gt;

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

&lt;p&gt;Magento's indexers create flat tables like &lt;code&gt;catalog_product_flat_1&lt;/code&gt; that denormalize EAV data into a single row per product. These tables are what actually power category pages and search results.&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;-- Flat table structure (simplified)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sku&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;price&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;visibility&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url_key&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;catalog_product_flat_1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;visibility&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&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;position&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query is fast because it hits a single flat table with standard column indexes — no EAV joins needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Critical Indexes Every Magento Store Needs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Sales Order Indexes
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;sales_order&lt;/code&gt; and &lt;code&gt;sales_order_grid&lt;/code&gt; tables are queried heavily in the admin panel and for order processing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check existing indexes&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_order_grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Add composite index for admin order grid filtering&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales_order_grid&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_status_created&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;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Add index for customer order history&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales_order&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_customer_id_created&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without these, loading the admin order grid with 500K+ orders takes 10-30 seconds. With them, it drops to under 1 second.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Catalog Product Indexes
&lt;/h3&gt;

&lt;p&gt;The flat product table needs indexes matching your common query patterns:&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;-- For category pages (most common query)&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_flat_1&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_status_visibility_position&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;visibility&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- For product search/filter by attribute set&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_flat_1&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_attribute_set_id&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute_set_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- For price-based sorting and filtering&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_flat_1&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_price&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Cart and Quote Indexes
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;quote&lt;/code&gt; and &lt;code&gt;quote_item&lt;/code&gt; tables slow down as abandoned carts accumulate:&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;-- Speed up quote lookups by store&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_store_id_is_active&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Speed up guest cart retrieval&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_customer_id_is_active&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Quote item lookups&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;quote_item&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_quote_id_product_id&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quote_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. URL Rewrite Indexes
&lt;/h3&gt;

&lt;p&gt;URL rewrites are hit on every page load. With thousands of products, the &lt;code&gt;url_rewrite&lt;/code&gt; table grows fast:&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;-- The critical lookup index&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;url_rewrite&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_request_path_store_id&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- For redirect lookups&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;url_rewrite&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_target_path_store_id&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using EXPLAIN to Identify Slow Queries
&lt;/h2&gt;

&lt;p&gt;Before adding indexes blindly, always analyze with &lt;code&gt;EXPLAIN&lt;/code&gt;. Here's how to read the output for Magento-specific queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_varchar&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eav_attribute&lt;/span&gt;
                           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;attribute_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;
                           &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;entity_type_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_decimal&lt;/span&gt; &lt;span class="n"&gt;d1&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;d1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eav_attribute&lt;/span&gt;
                           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;attribute_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'price'&lt;/span&gt;
                           &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;entity_type_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What to Look For in EXPLAIN Output
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;type: ALL&lt;/code&gt;&lt;/strong&gt; — Full table scan. This is the worst case. Add an index.&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;-- Bad: full table scan on 500K rows&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+-------+------+---------------+------+---------+------+--------+-------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;select_type&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;possible_keys&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;key_len&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Extra&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+-------+------+---------------+------+---------+------+--------+-------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;SIMPLE&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;          &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;487231&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+-------+------+---------------+------+---------+------+--------+-------+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;type: ref&lt;/code&gt;&lt;/strong&gt; — Index lookup using a reference. Good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;type: const&lt;/code&gt;&lt;/strong&gt; — Single row lookup by primary key. Best possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Extra: Using filesort&lt;/code&gt;&lt;/strong&gt; — MySQL is sorting results manually. Often means the ORDER BY doesn't match an index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Extra: Using temporary&lt;/code&gt;&lt;/strong&gt; — Creating a temporary table. Often unavoidable with complex joins but try to minimize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Magento-Specific Query Patterns to Optimize
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Category Product Listing
&lt;/h3&gt;

&lt;p&gt;The most queried page on most stores. Here's the typical pattern:&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;-- Default Magento category query (simplified)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sku&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;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thumbnail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url_key&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;catalog_product_flat_1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;visibility&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;catalog_category_product&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;category_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
  &lt;span class="p"&gt;))&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;position&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subquery in &lt;code&gt;IN (...)&lt;/code&gt; is the performance trap. For large categories, optimize with a JOIN:&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;-- Optimized with JOIN instead of IN subquery&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thumbnail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url_key&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;catalog_product_flat_1&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;catalog_category_product&lt;/span&gt; &lt;span class="n"&gt;ccp&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ccp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibility&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;ccp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ccp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;position&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the necessary index on the category-product relation table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_category_product&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_category_id_position&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Customer Group Price Queries
&lt;/h3&gt;

&lt;p&gt;Tier prices and group prices are loaded per product. With many customer groups, this explodes:&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;-- Add index for tier price lookups&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_tier_price&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_product_id_customer_group&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&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;qty&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Add index for catalog rule price&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalogrule_product_price&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_product_id_rule_date&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_group_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Inventory / Stock Queries
&lt;/h3&gt;

&lt;p&gt;MSI (Multi-Source Inventory) adds complexity. The &lt;code&gt;inventory_source_item&lt;/code&gt; table needs proper indexing:&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;-- For stock status checks&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;inventory_source_item&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_sku_source&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- For stock aggregation&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;inventory_stock_1&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_product_id_stock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_salable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Monitoring Slow Queries in Production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Enable the MySQL Slow Query Log
&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;# my.cnf
&lt;/span&gt;&lt;span class="nn"&gt;[mysqld]&lt;/span&gt;
&lt;span class="py"&gt;slow_query_log&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;slow_query_log_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/mysql/slow.log&lt;/span&gt;
&lt;span class="py"&gt;long_query_time&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;log_queries_not_using_indexes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;min_examined_row_limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After enabling, watch for Magento patterns:&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;# Top 10 slowest queries from the log&lt;/span&gt;
pt-query-digest /var/log/mysql/slow.log &lt;span class="nt"&gt;--limit&lt;/span&gt; 10

&lt;span class="c"&gt;# Or manually&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Query_time"&lt;/span&gt; /var/log/mysql/slow.log | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt;: &lt;span class="nt"&gt;-k2&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;h3&gt;
  
  
  Using Magento's Built-in Profiler
&lt;/h3&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'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;'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;'magento2'&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;'root'&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;'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 utf8mb4;'&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 log the profiler output in a custom module or check the &lt;code&gt;var/debug/db.log&lt;/code&gt; file for query times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connection Pooling and Persistent Connections
&lt;/h2&gt;

&lt;p&gt;Magento opens a new MySQL connection per request by default. Under high traffic, connection establishment becomes a bottleneck:&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 — enable persistent connections&lt;/span&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;'driver_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="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_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="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 even better results, use ProxySQL as a connection pool between PHP and MySQL:&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;# Install ProxySQL&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install &lt;/span&gt;proxysql

&lt;span class="c"&gt;# /etc/proxysql.cnf&lt;/span&gt;
mysql_users &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;{&lt;/span&gt;
        username &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"magento"&lt;/span&gt;
        password &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"secret"&lt;/span&gt;
        default_hostgroup &lt;span class="o"&gt;=&lt;/span&gt; 0
        max_connections &lt;span class="o"&gt;=&lt;/span&gt; 200
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ProxySQL maintains a pool of persistent connections and routes queries efficiently — especially useful when running multiple PHP-FPM workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  InnoDB Buffer Pool Optimization
&lt;/h2&gt;

&lt;p&gt;The InnoDB buffer pool is MySQL's most important memory setting. It caches data and indexes in RAM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# my.cnf — set to 70-80% of available RAM for dedicated DB server
&lt;/span&gt;&lt;span class="nn"&gt;[mysqld]&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;12G&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  # One per GB, up to 64&lt;/span&gt;
&lt;span class="py"&gt;innodb_log_file_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;2G&lt;/span&gt;
&lt;span class="py"&gt;innodb_flush_log_at_trx_commit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;2  # Slight risk, big speed gain&lt;/span&gt;
&lt;span class="py"&gt;innodb_flush_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;O_DIRECT&lt;/span&gt;
&lt;span class="py"&gt;innodb_io_capacity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;2000  # For SSD&lt;/span&gt;
&lt;span class="py"&gt;innodb_io_capacity_max&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;4000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check if your buffer pool is large enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&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;'Innodb_buffer_pool_%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Key metrics:&lt;/span&gt;
&lt;span class="c1"&gt;-- Innodb_buffer_pool_read_requests / Innodb_buffer_pool_reads = hit ratio&lt;/span&gt;
&lt;span class="c1"&gt;-- Should be &amp;gt; 99%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the hit ratio drops below 99%, increase &lt;code&gt;innodb_buffer_pool_size&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table Maintenance and Fragmentation
&lt;/h2&gt;

&lt;p&gt;Magento tables fragment over time as products are added, updated, and deleted. Regular maintenance keeps them lean:&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 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;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="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;data_mb&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_free&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;fragmented_mb&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_free&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;data_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;100&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;frag_pct&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;'magento2'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;data_free&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;data_free&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Defragment a table&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;InnoDB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- or&lt;/span&gt;
&lt;span class="n"&gt;OPTIMIZE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this monthly during off-peak hours. For zero-downtime optimization, use &lt;code&gt;pt-online-schema-change&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;pt-online-schema-change &lt;span class="nt"&gt;--alter&lt;/span&gt; &lt;span class="s2"&gt;"ENGINE=InnoDB"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;magento2,t&lt;span class="o"&gt;=&lt;/span&gt;catalog_product_entity &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--execute&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;After implementing these index and query optimizations on a store with 120K SKUs and 2M orders:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Category page load&lt;/td&gt;
&lt;td&gt;3.2s&lt;/td&gt;
&lt;td&gt;0.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin order grid&lt;/td&gt;
&lt;td&gt;12s&lt;/td&gt;
&lt;td&gt;0.9s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout (place order)&lt;/td&gt;
&lt;td&gt;8s&lt;/td&gt;
&lt;td&gt;1.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search results&lt;/td&gt;
&lt;td&gt;2.1s&lt;/td&gt;
&lt;td&gt;0.6s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg MySQL queries/page&lt;/td&gt;
&lt;td&gt;340&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest wins came from the flat table indexes, replacing IN subqueries with JOINs, and the InnoDB buffer pool increase.&lt;/p&gt;

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

&lt;p&gt;Database optimization is rarely glamorous, but it's where the most impactful performance gains hide. Start with &lt;code&gt;EXPLAIN&lt;/code&gt; on your slowest pages, add targeted indexes, tune your InnoDB settings, and maintain your tables regularly. Your Redis and Varnish caches work better when the database underneath them is fast — because cache misses are inevitable, and when they happen, you want the database to respond in milliseconds, not seconds.&lt;/p&gt;

&lt;p&gt;Monitor your slow query log weekly, revisit your indexes after major catalog changes, and always test schema changes in staging before production. A well-tuned MySQL layer is the foundation that every other Magento optimization sits on top of.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>mysql</category>
      <category>performance</category>
      <category>database</category>
    </item>
    <item>
      <title>Magento 2 Search Autocomplete Performance: Keeping Sub-200ms at Scale</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Fri, 24 Jul 2026 09:08:09 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-search-autocomplete-performance-keeping-sub-200ms-at-scale-1kjl</link>
      <guid>https://dev.to/magevanta/magento-2-search-autocomplete-performance-keeping-sub-200ms-at-scale-1kjl</guid>
      <description>&lt;p&gt;Search autocomplete is the single most resource-intensive interactive feature on a Magento 2 storefront. Every keystroke in the search bar triggers an AJAX request to the backend, which queries the search engine, loads product data, applies permissions, and returns HTML. On a busy store with hundreds of concurrent visitors, autocomplete generates more backend requests than any other page — and almost none of it is cached.&lt;/p&gt;

&lt;p&gt;I've seen Magento 2 stores where autocomplete took 800ms to 1.5 seconds per request. On a category page with 200 concurrent users, that's enough to saturate your PHP-FPM pool and bring the entire site to its knees. The frustrating part: most of this overhead is completely unnecessary.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk through exactly how Magento 2 autocomplete works under the hood, where the bottlenecks hide, and how to bring response times under 200ms — even on large catalogs.&lt;/p&gt;

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

&lt;p&gt;When a customer types into the search field, Magento's JavaScript sends an AJAX request to &lt;code&gt;/search/ajax/suggest&lt;/code&gt;. Here's the request flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript intercepts keystroke&lt;/strong&gt; — &lt;code&gt;quickSearch.js&lt;/code&gt; debounces input (default 300ms delay)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AJAX GET to &lt;code&gt;/search/ajax/suggest&lt;/code&gt;&lt;/strong&gt; — includes the search term and store scope&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search controller executes&lt;/strong&gt; — &lt;code&gt;Magento\Search\Controller\Ajax\Suggest::execute()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search engine query fires&lt;/strong&gt; — Elasticsearch/OpenSearch gets a &lt;code&gt;multi_match&lt;/code&gt; query with fuzzy matching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product collection loads&lt;/strong&gt; — Full product objects are loaded for each search result (5-10 products)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTML renders server-side&lt;/strong&gt; — Each product goes through layout XML, block rendering, and template output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response returns&lt;/strong&gt; — The HTML fragment is sent back as JSON&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The critical issue: &lt;strong&gt;steps 4-6 run on every request with zero caching&lt;/strong&gt;. A typical autocomplete request loads 5-10 full product objects with EAV attributes, generates layout blocks, and renders templates — identical work to what happens on a category page, except it's uncached and fires every 300ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Bottlenecks Hide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Product Collection Loading (60-70% of response time)
&lt;/h3&gt;

&lt;p&gt;The autocomplete response loads products using a standard collection with EAV. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5-10 &lt;code&gt;SELECT&lt;/code&gt; queries for product entities&lt;/li&gt;
&lt;li&gt;50-100+ &lt;code&gt;SELECT&lt;/code&gt; queries for EAV attribute values (name, price, image, description, etc.)&lt;/li&gt;
&lt;li&gt;Additional queries for tier prices, stock status, category associations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a catalog with 50,000+ SKUs, these queries hit buffer pool pressure. Even with indexes, the EAV &lt;code&gt;SELECT&lt;/code&gt; queries require joining &lt;code&gt;catalog_product_entity_varchar&lt;/code&gt;, &lt;code&gt;catalog_product_entity_decimal&lt;/code&gt;, &lt;code&gt;catalog_product_entity_int&lt;/code&gt;, and more — one per attribute per product.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Enable the general query log temporarily&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;general_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ON'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Trigger an autocomplete request, then count queries&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;general_log&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;command_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Query'&lt;/span&gt; 
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;SECOND&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single autocomplete request typically generates &lt;strong&gt;80-150 SQL queries&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Layout XML and Block Rendering
&lt;/h3&gt;

&lt;p&gt;Magento renders the autocomplete HTML using a full layout XML pipeline. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loading layout XML handles&lt;/li&gt;
&lt;li&gt;Merging &lt;code&gt;catalogsearch_ajax_suggest.xml&lt;/code&gt; with default handles&lt;/li&gt;
&lt;li&gt;Instantiating block classes&lt;/li&gt;
&lt;li&gt;Running &lt;code&gt;toHtml()&lt;/code&gt; on each block&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The layout merge alone takes 10-30ms. Block rendering adds another 20-50ms depending on template complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Search Engine Query Design
&lt;/h3&gt;

&lt;p&gt;The default Elasticsearch query for autocomplete uses &lt;code&gt;multi_match&lt;/code&gt; with &lt;code&gt;best_fields&lt;/code&gt; type and fuzzy matching. On large catalogs, this is unnecessarily expensive:&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;"query"&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;span class="nl"&gt;"multi_match"&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;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search term"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fields"&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="s2"&gt;"name^3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sku^2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"best_fields"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fuzziness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AUTO"&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;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&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;The &lt;code&gt;fuzziness: AUTO&lt;/code&gt; parameter is the biggest performance killer. It generates edit-distance calculations for every term against every document. On a 100,000 SKU catalog, that's a lot of computation per keystroke.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: Reduce EAV Attribute Loading
&lt;/h2&gt;

&lt;p&gt;The default autocomplete loads all searchable attributes. You can drastically reduce this by limiting which attributes are loaded:&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;!-- app/code/Vendor/Module/view/frontend/layout/catalogsearch_ajax_suggest.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;page&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:View/Layout/etc/page_configuration.xsd"&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;referenceBlock&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"search_result_list"&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;"search_result_list"&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;"attributes_to_select"&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;"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;name&lt;span class="nt"&gt;&amp;lt;/item&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;"sku"&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;sku&lt;span class="nt"&gt;&amp;lt;/item&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;"price"&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;price&lt;span class="nt"&gt;&amp;lt;/item&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;"thumbnail"&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;thumbnail&lt;span class="nt"&gt;&amp;lt;/item&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;"url_key"&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;url_key&lt;span class="nt"&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
                    &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;/referenceBlock&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/page&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or override the product collection directly in a plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/code/Vendor/Module/Plugin/SearchProductProviderPlugin.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Vendor\Module\Plugin&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\CatalogSearch\Model\Search\ProductProvider&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;SearchProductProviderPlugin&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * Strip heavy attributes from autocomplete product loading
     */&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;afterGetList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ProductProvider&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'short_description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'meta_title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'meta_description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'meta_keyword'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Impact:&lt;/strong&gt; Reduces SQL queries from 80-150 down to 30-50 per autocomplete request. Response time drops by 40-60%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: Tune the Elasticsearch Query
&lt;/h2&gt;

&lt;p&gt;The default fuzzy query is overkill for autocomplete. Replace it with a more efficient &lt;code&gt;match_phrase_prefix&lt;/code&gt; or use a dedicated search-as-you-type field:&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: Use match_phrase_prefix Instead of Fuzzy
&lt;/h3&gt;

&lt;p&gt;Override the search request to use prefix matching, which leverages the inverted index directly:&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;!-- app/etc/elasticsearch/custom_search_request.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;requests&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;request&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"quick_search_container"&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Quick Search"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;queries&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;query&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"quick_search_container"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"boolQuery"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;queryReference&lt;/span&gt; &lt;span class="na"&gt;clause=&lt;/span&gt;&lt;span class="s"&gt;"should"&lt;/span&gt; &lt;span class="na"&gt;ref=&lt;/span&gt;&lt;span class="s"&gt;"search_query"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;queryReference&lt;/span&gt; &lt;span class="na"&gt;clause=&lt;/span&gt;&lt;span class="s"&gt;"should"&lt;/span&gt; &lt;span class="na"&gt;ref=&lt;/span&gt;&lt;span class="s"&gt;"sku_query"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/query&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;query&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"search_query"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"matchQuery"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;match&lt;/span&gt; &lt;span class="na"&gt;field=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"match_phrase_prefix"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/query&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;query&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"sku_query"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"matchQuery"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;match&lt;/span&gt; &lt;span class="na"&gt;field=&lt;/span&gt;&lt;span class="s"&gt;"sku"&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"match"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/query&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/queries&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/request&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/requests&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option B: Use search_as_you_type Field Mapping
&lt;/h3&gt;

&lt;p&gt;Create a custom field mapping that uses Elasticsearch's &lt;code&gt;search_as_you_type&lt;/code&gt; field type:&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="err"&gt;PUT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/magento&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;_product/_mapping&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;span class="nl"&gt;"properties"&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;span class="nl"&gt;"name_suggest"&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search_as_you_type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"max_shingle_size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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;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;Then query it with &lt;code&gt;bool_prefix&lt;/code&gt;:&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;"query"&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;span class="nl"&gt;"multi_match"&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;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search term"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fields"&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;span class="s2"&gt;"name_suggest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"name_suggest._2gram"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"name_suggest._3gram"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"sku"&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bool_prefix"&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;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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;&lt;strong&gt;Impact:&lt;/strong&gt; &lt;code&gt;search_as_you_type&lt;/code&gt; with &lt;code&gt;bool_prefix&lt;/code&gt; is 5-10x faster than &lt;code&gt;multi_match&lt;/code&gt; with &lt;code&gt;fuzziness: AUTO&lt;/code&gt; because it uses pre-computed n-gram indexes instead of runtime edit-distance calculations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: Cache Autocomplete Results
&lt;/h2&gt;

&lt;p&gt;The simplest high-impact optimization: cache autocomplete responses. Most users search for the same popular terms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/code/Vendor/Module/Plugin/AjaxSuggestCachePlugin.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Vendor\Module\Plugin&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\Search\Controller\Ajax\Suggest&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\App\Response\Http&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\App\CacheInterface&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;AjaxSuggestCachePlugin&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;const&lt;/span&gt; &lt;span class="no"&gt;CACHE_TAG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'search_autocomplete'&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;const&lt;/span&gt; &lt;span class="no"&gt;CACHE_LIFETIME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1 hour&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;CacheInterface&lt;/span&gt; &lt;span class="nv"&gt;$cache&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;aroundExecute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Suggest&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;callable&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Http&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRequest&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;getParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'q'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$storeId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRequest&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;getParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'store_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&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="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&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;$proceed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'autocomplete_%d_%s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$storeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cached&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getResponse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cached&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Cache-Control'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'public, max-age=60'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Cache'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'HIT'&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;$response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getBody&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CACHE_TAG&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CACHE_LIFETIME&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Cache'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'MISS'&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;$response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register the plugin:&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;!-- app/code/Vendor/Module/etc/frontend/di.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:ObjectManager/etc/config.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Search\Controller\Ajax\Suggest"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;plugin&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"autocomplete_cache"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"Vendor\Module\Plugin\AjaxSuggestCachePlugin"&lt;/span&gt; &lt;span class="na"&gt;sortOrder=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add cache invalidation when products are updated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/code/Vendor/Module/Observer/InvalidateAutocompleteCache.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Vendor\Module\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\Event\ObserverInterface&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\Event\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\App\CacheInterface&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;InvalidateAutocompleteCache&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;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="kt"&gt;CacheInterface&lt;/span&gt; &lt;span class="nv"&gt;$cache&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Observer&lt;/span&gt; &lt;span class="nv"&gt;$observer&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;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nc"&gt;\Vendor\Module\Plugin\AjaxSuggestCachePlugin&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Impact:&lt;/strong&gt; For popular search terms (which cover 80%+ of queries), this eliminates the backend entirely. Response time goes from 200-500ms to under 10ms on cache hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 4: Debounce and Client-Side Optimization
&lt;/h2&gt;

&lt;p&gt;The default Magento debounce is 300ms. On fast typists, this still generates a lot of requests. Increase it and add minimum character requirements:&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;// app/code/Vendor/Module/view/frontend/requirejs-config.js&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;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;mixins&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_CatalogSearch/js/form-mini&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;Vendor_Module/js/search-mixin&lt;/span&gt;&lt;span class="dl"&gt;'&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;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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/code/Vendor/Module/view/frontend/web/js/search-mixin.js&lt;/span&gt;
&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="nf"&gt;function &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;use strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&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="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;searchDelay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Increased from 300ms&lt;/span&gt;
                &lt;span class="na"&gt;minSearchLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;      &lt;span class="c1"&gt;// Require at least 3 characters&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;&lt;strong&gt;Additional client-side optimizations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Abort previous requests&lt;/strong&gt; — When a new keystroke fires, abort the in-flight AJAX request. Magento's default implementation does this, but verify it's working.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache results in localStorage&lt;/strong&gt; — For repeat visitors, store recent autocomplete results client-side with a 5-minute TTL. This eliminates server roundtrips entirely for repeated searches.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Show results from cache instantly&lt;/strong&gt; — Display cached results immediately, then update in the background if the API returns new data.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Fix 5: Limit Result Count and Simplify Templates
&lt;/h2&gt;

&lt;p&gt;The default autocomplete shows 5-10 products. Reducing this to 3-5 cuts rendering time proportionally:&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;!-- app/etc/view.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;vars&lt;/span&gt; &lt;span class="na"&gt;module=&lt;/span&gt;&lt;span class="s"&gt;"Magento_CatalogSearch"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;var&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"max_query_length"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/var&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/vars&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strip heavy elements from the autocomplete template. Skip rating summary, tier prices, and swatches — they all trigger additional queries. A simplified template with just image, name, and price eliminates 3-7 SQL queries per product. With 5 products shown, that's 15-35 fewer queries per request.&lt;/p&gt;

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

&lt;p&gt;Use a load testing tool to compare before and after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Benchmark with 100 concurrent connections, 1000 requests&lt;/span&gt;
ab &lt;span class="nt"&gt;-n&lt;/span&gt; 1000 &lt;span class="nt"&gt;-c&lt;/span&gt; 100 &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="s2"&gt;"https://your-store.com/search/ajax/suggest?q=shirt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected results:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Avg response time&lt;/td&gt;
&lt;td&gt;350-800ms&lt;/td&gt;
&lt;td&gt;50-150ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P95 response time&lt;/td&gt;
&lt;td&gt;1.2-2.5s&lt;/td&gt;
&lt;td&gt;200-400ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL queries per request&lt;/td&gt;
&lt;td&gt;80-150&lt;/td&gt;
&lt;td&gt;15-30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache hit rate&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;60-80%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Production Checklist
&lt;/h2&gt;

&lt;p&gt;Before deploying these changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Test autocomplete accuracy&lt;/strong&gt; — Ensure results are still relevant after disabling fuzzy matching. If relevance drops too much, use &lt;code&gt;match_phrase_prefix&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Monitor Elasticsearch cluster health&lt;/strong&gt; — Changing query patterns can shift load. Watch CPU, heap, and query latency.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Set up cache warming&lt;/strong&gt; — Pre-warm autocomplete cache with top 100-200 search terms from your analytics.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Add cache invalidation&lt;/strong&gt; — Product updates, stock changes, and price changes should trigger selective cache clearing.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Test with slow connections&lt;/strong&gt; — Autocomplete UX degrades badly on 3G. Consider showing a loading indicator.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Audit third-party modules&lt;/strong&gt; — Some search extensions (ElasticSuite, Amasty, Mirasvit) override the default autocomplete. Check their configurations first.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Search autocomplete is one of those features that "just works" in development but falls apart at scale. The combination of uncached EAV loading, expensive fuzzy search queries, and full layout rendering makes every keystroke a mini page load.&lt;/p&gt;

&lt;p&gt;The highest-impact fixes, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cache autocomplete responses&lt;/strong&gt; (60-80% of requests become instant)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce product attribute loading&lt;/strong&gt; (40-60% reduction in query time)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tune Elasticsearch queries&lt;/strong&gt; (5-10x faster search execution)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplify templates&lt;/strong&gt; (eliminate unnecessary product data queries)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Increase debounce and add client-side caching&lt;/strong&gt; (reduce request volume by 40-50%)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Combined, these optimizations bring autocomplete from 500-1500ms down to 50-150ms. Your customers get instant suggestions, your server handles 3-5x more concurrent searchers, and your Core Web Vitals scores stop bleeding.&lt;/p&gt;

&lt;p&gt;The search bar is your store's front door. Make sure it opens fast.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>performance</category>
      <category>elasticsearch</category>
      <category>php</category>
    </item>
    <item>
      <title>Magento 2 Dependency Injection Performance: Proxy Classes, Compiled DI, and Object Manager Pitfalls</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Thu, 23 Jul 2026 09:07:35 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-dependency-injection-performance-proxy-classes-compiled-di-and-object-manager-pitfalls-47nh</link>
      <guid>https://dev.to/magevanta/magento-2-dependency-injection-performance-proxy-classes-compiled-di-and-object-manager-pitfalls-47nh</guid>
      <description>&lt;p&gt;Magento 2's dependency injection (DI) system is one of its most powerful architectural features, but it can silently become a performance bottleneck if misused. Understanding how the DI container works internally — from compilation to runtime resolution — is essential for building high-performance Magento applications. In this post, we'll explore the hidden performance costs of DI, how proxy classes save initialization overhead, and why the object manager is a trap you should avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Magento 2 DI Works Under the Hood
&lt;/h2&gt;

&lt;p&gt;At the core of Magento 2's DI system is the &lt;code&gt;ObjectManager&lt;/code&gt;, a service container that instantiates classes and resolves their dependencies. However, unlike a naive runtime container, Magento compiles DI information ahead of time to improve performance.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;bin/magento setup:di:compile&lt;/code&gt;, Magento reads all constructor signatures in the codebase and generates &lt;code&gt;generated/metadata/compiled.php&lt;/code&gt; — a massive array mapping every class to its dependencies. It also creates factory and interceptor classes in &lt;code&gt;generated/code/&lt;/code&gt;. This compilation step is what makes Magento 2's DI performant in production, but it comes with tradeoffs.&lt;/p&gt;

&lt;p&gt;Without compilation, Magento would need to use PHP reflection at runtime to inspect constructors, determine parameter types, and build dependency trees. Reflection is computationally expensive and would add significant latency to every request. The compiled metadata eliminates this overhead entirely by pre-computing the dependency graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost of Constructor Dependency Bloat
&lt;/h2&gt;

&lt;p&gt;A common anti-pattern in Magento 2 is injecting far too many dependencies into a single class. Every dependency in a constructor requires the DI container to instantiate (or fetch from cache) that object and all of its transitive dependencies. A class with 15 constructor parameters can trigger a cascade of dozens of object instantiations before its own constructor even runs.&lt;/p&gt;

&lt;p&gt;This is particularly problematic in frontend contexts where response time directly impacts user experience. A plugin or observer that injects a heavy service like the &lt;code&gt;SearchCriteriaBuilder&lt;/code&gt; or &lt;code&gt;ProductRepository&lt;/code&gt; might seem harmless in isolation, but when multiplied across dozens of plugins firing on every request, the cumulative overhead becomes significant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule of thumb:&lt;/strong&gt; inject only what you need. If a dependency is only used in one method, consider using a factory or fetching it lazily rather than injecting it in the constructor. This keeps the instantiation chain lightweight and prevents unnecessary object creation on every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proxy Classes: The Lazy Initialization Pattern
&lt;/h2&gt;

&lt;p&gt;Magento 2 provides a powerful but underutilized feature: &lt;strong&gt;proxy classes&lt;/strong&gt;. A proxy is a lightweight wrapper around a real service that delays its actual instantiation until a method is called on it. You can create a proxy by simply adding &lt;code&gt;Proxy&lt;/code&gt; to the end of a class name in your constructor injection.&lt;/p&gt;

&lt;p&gt;For example, if your class needs a heavy dependency like &lt;code&gt;Magento\Catalog\Model\ProductRepository&lt;/code&gt;, you can inject &lt;code&gt;Magento\Catalog\Model\ProductRepository\Proxy&lt;/code&gt; instead. The DI container will inject a proxy object that does nothing on construction. Only when you actually call a method on the proxy does it instantiate the real &lt;code&gt;ProductRepository&lt;/code&gt; and forward the call.&lt;/p&gt;

&lt;p&gt;This pattern is invaluable for classes that have conditional logic — where a heavy dependency is only needed in some code paths. Without a proxy, the DI container instantiates the dependency regardless of whether it's used. With a proxy, you pay the initialization cost only when the dependency is actually accessed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use proxies:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For heavy services with expensive constructors (repositories, complex models, external API clients)&lt;/li&gt;
&lt;li&gt;In plugins or observers where the injected service is only used in specific conditions&lt;/li&gt;
&lt;li&gt;For rarely-used features where eager instantiation is wasteful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When NOT to use proxies:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For lightweight services where the proxy overhead exceeds the savings&lt;/li&gt;
&lt;li&gt;When the dependency is always used — a proxy adds an extra method call layer for no benefit&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Compiled DI vs. Runtime DI
&lt;/h2&gt;

&lt;p&gt;Magento 2 supports two DI modes: compiled and runtime. In production, you should always use compiled DI via &lt;code&gt;setup:di:compile&lt;/code&gt;. Runtime DI exists primarily for development convenience, but it carries a severe performance penalty.&lt;/p&gt;

&lt;p&gt;In runtime mode, Magento falls back to reflection for every class instantiation. This means parsing PHP docblocks, inspecting type hints, and building dependency trees on the fly. On a warm production cache, a single class instantiation via compiled DI might take microseconds; the same class via runtime DI could take milliseconds. Multiply that across hundreds of objects per request and the difference is dramatic.&lt;/p&gt;

&lt;p&gt;The compiled DI system also enables optimizations like shared instance reuse. When multiple classes depend on the same singleton service, the compiled container caches the first instantiation and returns the same instance for subsequent requests. Runtime DI may not guarantee this caching behavior, leading to duplicate object creation and increased memory pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance tip:&lt;/strong&gt; always run &lt;code&gt;setup:di:compile&lt;/code&gt; before deploying to production, and ensure your deployment pipeline never skips this step. A missing compilation can easily double or triple your response times without any obvious error in logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Object Manager Anti-Pattern
&lt;/h2&gt;

&lt;p&gt;Perhaps the most common DI-related performance mistake in Magento 2 is direct object manager usage. You'll often see code like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$objectManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;\Magento\Framework\App\ObjectManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$productRepository&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\Catalog\Model\ProductRepository&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an anti-pattern for several reasons. First, it bypasses the compiled DI metadata entirely, forcing Magento to resolve dependencies at runtime through reflection. Second, it makes the code untestable and couples it to the global object manager instance. Third, it prevents Magento from applying important optimizations like proxy resolution and shared instance caching.&lt;/p&gt;

&lt;p&gt;The object manager also lacks awareness of lifecycle management. When you use constructor injection, Magento can apply proxies, interceptors, and preference mappings configured in &lt;code&gt;di.xml&lt;/code&gt;. When you use the object manager directly, you get a raw instance without any of these layers — which often means missing performance optimizations that the framework would otherwise apply automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is simple:&lt;/strong&gt; always use constructor injection. If you need a dependency that varies at runtime, use a factory class injected via the constructor. Magento's factory pattern (&lt;code&gt;Vendor\Module\Model\SomethingFactory&lt;/code&gt;) is specifically designed for this use case and integrates cleanly with the compiled DI system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impact on Production Workloads
&lt;/h2&gt;

&lt;p&gt;On high-traffic Magento 2 stores, DI performance issues compound quickly. A single slow constructor chain might add 50ms to a request — negligible in isolation, but when multiplied across thousands of concurrent requests, it translates to higher CPU load, increased memory consumption, and slower overall throughput.&lt;/p&gt;

&lt;p&gt;We've seen stores where removing just three unnecessary dependencies from a frequently-executed plugin reduced average response time by 12%. Replacing eager repository instantiation with proxies in a checkout observer cut page load time by nearly 200ms for users with large carts. These aren't edge cases — they're the kind of wins that come from understanding how DI works at a fundamental level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inject only what you need&lt;/strong&gt; — avoid constructor bloat; use factories for conditional dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use proxy classes&lt;/strong&gt; for heavy, conditionally-used services to enable lazy initialization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always compile DI&lt;/strong&gt; in production via &lt;code&gt;setup:di:compile&lt;/code&gt;; never rely on runtime DI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never use the object manager directly&lt;/strong&gt; — prefer constructor injection or factories&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit constructor signatures&lt;/strong&gt; during code reviews; treat excess dependencies as a red flag&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile with Blackfire or New Relic&lt;/strong&gt; to identify slow instantiation chains in your custom code&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Magento 2's dependency injection system is sophisticated and powerful, but its performance characteristics aren't always obvious. The difference between well-optimized DI usage and careless injection patterns can translate to hundreds of milliseconds per request — a meaningful impact on conversion rates and user experience.&lt;/p&gt;

&lt;p&gt;By leveraging compiled DI, proxy classes, and proper constructor discipline, you can ensure your Magento 2 application remains fast and scalable without sacrificing the flexibility that makes the framework powerful. Treat your constructors as a performance contract: every dependency you inject is a promise that the container will instantiate it, and that cost is paid on every request where your class is used.&lt;/p&gt;

</description>
      <category>magento</category>
    </item>
    <item>
      <title>Magento 2 Crawler &amp; Bot Traffic Management — Stop Bad Bots From Destroying Your Performance</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:07:08 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-crawler-bot-traffic-management-stop-bad-bots-from-destroying-your-performance-76i</link>
      <guid>https://dev.to/magevanta/magento-2-crawler-bot-traffic-management-stop-bad-bots-from-destroying-your-performance-76i</guid>
      <description>&lt;p&gt;Bot traffic is the invisible tax on Magento 2 performance. While human shoppers browse a few pages per session, bots crawl thousands of URLs, bypass caches, and strain your infrastructure — often without you knowing. A single misbehaving crawler can double your server load and spike your CDN bill.&lt;/p&gt;

&lt;p&gt;This post covers everything you need to detect bot traffic, optimize for legitimate crawlers, and block the harmful ones. We'll look at practical Nginx, Varnish, and application-level solutions you can deploy today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bots Matter for Magento 2 Performance
&lt;/h2&gt;

&lt;p&gt;Magento is a complex platform. Every uncached page requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP-FPM workers to render templates&lt;/li&gt;
&lt;li&gt;MySQL queries for EAV product data&lt;/li&gt;
&lt;li&gt;Redis lookups for session and cache data&lt;/li&gt;
&lt;li&gt;Elasticsearch calls for layered navigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A human visitor might hit 5–10 pages per session. A bot can crawl 50,000 pages in an hour. The difference is staggering.&lt;/p&gt;

&lt;p&gt;Consider a store with 50,000 SKUs. A naive crawler following every product link, filter combination, and pagination page generates exponentially more requests than your actual customer base.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical bot impact:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache hit ratio drops from 85% to 40% because bots request unique URLs&lt;/li&gt;
&lt;li&gt;MySQL connection pool exhaustion from concurrent &lt;code&gt;catalog_product_entity&lt;/code&gt; lookups&lt;/li&gt;
&lt;li&gt;Elasticsearch cluster CPU spikes from facet aggregation queries&lt;/li&gt;
&lt;li&gt;CDN egress charges from uncached, CPU-rendered pages served to bots&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your analytics show "direct traffic" with a 100% bounce rate and 0s time-on-page, you might be looking at bot traffic disguised as humans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identifying Bot Traffic in Your Logs
&lt;/h2&gt;

&lt;p&gt;Before you fix anything, you need to know what you're dealing with. Start with your access logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nginx Access Log Analysis
&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;# Top user agents by request count&lt;/span&gt;
&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="s1"&gt;'{print $6}'&lt;/span&gt; /var/log/nginx/access.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;span class="c"&gt;# Requests per IP address&lt;/span&gt;
&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt; /var/log/nginx/access.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;span class="c"&gt;# Bot-only traffic volume&lt;/span&gt;
&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/Googlebot|bingbot|AhrefsBot|SemrushBot|MJ12bot/ {sum++} END {print sum}'&lt;/span&gt; /var/log/nginx/access.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Magento Application-Level Detection
&lt;/h3&gt;

&lt;p&gt;Log suspicious patterns from within Magento by extending the &lt;code&gt;Http&lt;/code&gt; context or using an observer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Vendor\BotLogger\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\Event\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\Event\ObserverInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Magento\Framework\HTTP\PhpEnvironment\Request&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;LogBotTraffic&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;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="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;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;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Observer&lt;/span&gt; &lt;span class="nv"&gt;$observer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$userAgent&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;request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'User-Agent'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$ip&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;request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getClientIp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Detect known bots&lt;/span&gt;
        &lt;span class="nv"&gt;$bots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Googlebot'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bingbot'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'AhrefsBot'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'SemrushBot'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'MJ12bot'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DotBot'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bots&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$bot&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="nb"&gt;stripos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'BOT_TRAFFIC'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="s1"&gt;'bot'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'ip'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRequestUri&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                    &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;]);&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="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;Track this in a separate log file and ingest it into your monitoring stack (Grafana, New Relic, or CloudWatch).&lt;/p&gt;

&lt;h2&gt;
  
  
  robots.txt — Your First Line of Defense
&lt;/h2&gt;

&lt;p&gt;A well-crafted &lt;code&gt;robots.txt&lt;/code&gt; is the simplest way to guide legitimate crawlers away from expensive pages.&lt;/p&gt;

&lt;p&gt;Place &lt;code&gt;robots.txt&lt;/code&gt; at &lt;code&gt;pub/robots.txt&lt;/code&gt; (or root if your webroot is &lt;code&gt;pub/&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;User&lt;/span&gt;-&lt;span class="n"&gt;agent&lt;/span&gt;: *
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;admin&lt;/span&gt;/
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;checkout&lt;/span&gt;/
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;customer&lt;/span&gt;/
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;catalogsearch&lt;/span&gt;/
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;sales&lt;/span&gt;/&lt;span class="n"&gt;guest&lt;/span&gt;/
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;wishlist&lt;/span&gt;/
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;compare&lt;/span&gt;/
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;review&lt;/span&gt;/&lt;span class="n"&gt;product&lt;/span&gt;/&lt;span class="n"&gt;post&lt;/span&gt;/
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;newsletter&lt;/span&gt;/
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;sendfriend&lt;/span&gt;/
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /&lt;span class="n"&gt;catalog&lt;/span&gt;/&lt;span class="n"&gt;product_compare&lt;/span&gt;/
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /*?*&lt;span class="n"&gt;price&lt;/span&gt;=
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /*?*&lt;span class="n"&gt;color&lt;/span&gt;=
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /*?*&lt;span class="n"&gt;size&lt;/span&gt;=
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /*?*&lt;span class="n"&gt;material&lt;/span&gt;=
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /*?*&lt;span class="n"&gt;limit&lt;/span&gt;=
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /*?*&lt;span class="n"&gt;order&lt;/span&gt;=
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /*?*&lt;span class="n"&gt;dir&lt;/span&gt;=
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /*?*&lt;span class="n"&gt;mode&lt;/span&gt;=

&lt;span class="c"&gt;# Allow CSS/JS/images for rendering
&lt;/span&gt;&lt;span class="n"&gt;Allow&lt;/span&gt;: /&lt;span class="n"&gt;pub&lt;/span&gt;/&lt;span class="n"&gt;media&lt;/span&gt;/&lt;span class="n"&gt;catalog&lt;/span&gt;/&lt;span class="n"&gt;product&lt;/span&gt;/
&lt;span class="n"&gt;Allow&lt;/span&gt;: /&lt;span class="n"&gt;pub&lt;/span&gt;/&lt;span class="n"&gt;static&lt;/span&gt;/

&lt;span class="c"&gt;# Sitemap for efficient crawling
&lt;/span&gt;&lt;span class="n"&gt;Sitemap&lt;/span&gt;: &lt;span class="n"&gt;https&lt;/span&gt;://&lt;span class="n"&gt;yourstore&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;/&lt;span class="n"&gt;sitemap&lt;/span&gt;.&lt;span class="n"&gt;xml&lt;/span&gt;

&lt;span class="c"&gt;# Crawl delay for all bots
&lt;/span&gt;&lt;span class="n"&gt;Crawl&lt;/span&gt;-&lt;span class="n"&gt;delay&lt;/span&gt;: &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key rules:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disallow layered navigation parameters&lt;/strong&gt; — price, color, size, material filters generate thousands of unique URLs with identical content. These are SEO nightmares and performance drains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disallow compare and wishlist&lt;/strong&gt; — these are user-specific and should never be crawled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set a crawl delay&lt;/strong&gt; — 2 seconds between requests from a single bot is reasonable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provide a sitemap&lt;/strong&gt; — direct crawlers to your canonical URLs instead of letting them discover every filter combination.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Magento-Specific robots.txt Pitfalls
&lt;/h3&gt;

&lt;p&gt;If your webroot is &lt;code&gt;pub/&lt;/code&gt; (recommended for security), &lt;code&gt;robots.txt&lt;/code&gt; lives at &lt;code&gt;pub/robots.txt&lt;/code&gt;. If your webroot is the Magento root, it goes there instead.&lt;/p&gt;

&lt;p&gt;Don't rely solely on &lt;code&gt;robots.txt&lt;/code&gt; for security — it's a polite request, not an enforcement mechanism. Malicious bots will ignore it entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nginx Rate Limiting for Bots
&lt;/h2&gt;

&lt;p&gt;Rate limiting at the web server level is your second line of defense. It protects PHP-FPM and MySQL from being overwhelmed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zone-Based Rate Limiting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In http context (nginx.conf)&lt;/span&gt;
&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=general:10m&lt;/span&gt; &lt;span class="s"&gt;rate=10r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$bot_zone&lt;/span&gt; &lt;span class="s"&gt;zone=bots:10m&lt;/span&gt; &lt;span class="s"&gt;rate=1r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Map user agents to bot classification&lt;/span&gt;
&lt;span class="k"&gt;map&lt;/span&gt; &lt;span class="nv"&gt;$http_user_agent&lt;/span&gt; &lt;span class="nv"&gt;$bot_zone&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;default&lt;/span&gt;                    &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;~*&lt;/span&gt;&lt;span class="s"&gt;(Googlebot|bingbot)&lt;/span&gt;      &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;~*&lt;/span&gt;&lt;span class="s"&gt;(AhrefsBot|SemrushBot)&lt;/span&gt;   &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;~*&lt;/span&gt;&lt;span class="s"&gt;(MJ12bot|DotBot|BLEXBot)&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;~*&lt;/span&gt;&lt;span class="s"&gt;(Screaming|Majestic)&lt;/span&gt;     &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;~*crawler&lt;/span&gt;                  &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;~*spider&lt;/span&gt;                  &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# In server context (site config)&lt;/span&gt;
&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# General limit for all traffic&lt;/span&gt;
    &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=general&lt;/span&gt; &lt;span class="s"&gt;burst=20&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Stricter limit for identified bots&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bot_zone&lt;/span&gt; &lt;span class="s"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"")&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=bots&lt;/span&gt; &lt;span class="s"&gt;burst=5&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.php&lt;/span&gt;&lt;span class="nv"&gt;$is_args$args&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Always allow static assets (they're cached and cheap)&lt;/span&gt;
    &lt;span class="kn"&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|css|js|svg|woff|woff2)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="no"&gt;off&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="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;
  
  
  What this achieves:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;General traffic&lt;/strong&gt;: 10 requests/second per IP with a burst of 20&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bots&lt;/strong&gt;: 1 request/second per IP with a burst of 5 — enough for legitimate crawlers, painful for aggressive ones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static assets&lt;/strong&gt;: No rate limiting — images and CSS should be served freely from cache&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dynamic Bot Blocking with Nginx + Lua
&lt;/h3&gt;

&lt;p&gt;For advanced setups, use OpenResty/Nginx with Lua to dynamically block bots based on behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- nginx.conf lua_shared_dict blocklist 10m;&lt;/span&gt;
&lt;span class="n"&gt;init_worker_by_lua_block&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;blocklist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blocklist&lt;/span&gt;
    &lt;span class="c1"&gt;-- Preload known bad actors&lt;/span&gt;
    &lt;span class="n"&gt;blocklist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"185.220.101.0/24"&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="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;access_by_lua_block&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;blocklist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blocklist&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remote_addr&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;blocklist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_FORBIDDEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;-- Block by excessive requests (detected via log analysis)&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;req_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;blocklist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;incr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;":count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="n"&gt;blocklist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&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="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;-- Block for 1 hour&lt;/span&gt;
        &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WARN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Auto-blocked IP for excessive requests: "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_TOO_MANY_REQUESTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Varnish Caching Strategy for Bots
&lt;/h2&gt;

&lt;p&gt;Varnish can differentiate between humans and bots, applying different caching policies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bot-Aware VCL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vcl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub&lt;/span&gt; &lt;span class="nf"&gt;vcl_recv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# Normalize user agent for bot detection&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.User-Agent&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s2"&gt;"(Googlebot|bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;req.http.X-Bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;req.http.X-Bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Don't cache admin, checkout, customer area for anyone&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.url&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s2"&gt;"^/(admin|checkout|customer)/"&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;pass&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Cache product pages for bots with longer TTL (they don't need fresh prices instantly)&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.X-Bot&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;req.url&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s2"&gt;"^/catalog/product/view/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;unset&lt;/span&gt; &lt;span class="nv"&gt;req.http.Cookie&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;hash&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# For humans, respect cookies&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.Cookie&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;# Strip all but essential cookies&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;req.http.Cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;regsuball&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.http.Cookie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"(^|;)\s*__[a-z]+=[^;]*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;req.http.Cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;regsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.http.Cookie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"^;\s*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&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;req.http.Cookie&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&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;unset&lt;/span&gt; &lt;span class="nv"&gt;req.http.Cookie&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="k"&gt;sub&lt;/span&gt; &lt;span class="nf"&gt;vcl_backend_response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# Cache product pages for bots longer&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;bereq.http.X-Bot&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;bereq.url&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s2"&gt;"^/catalog/product/view/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;beresp.ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mx"&gt;1h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;beresp.grace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mx"&gt;6h&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 category pages for everyone&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;bereq.url&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s2"&gt;"^/catalog/category/view/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;beresp.ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mx"&gt;15m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;beresp.grace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mx"&gt;1h&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;sub&lt;/span&gt; &lt;span class="nf"&gt;vcl_deliver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# Add debug header (remove in production)&lt;/span&gt;
    &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;resp.http.X-Cache-Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;req.http.X-Bot&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;
  
  
  Bot-specific caching benefits:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product pages&lt;/strong&gt;: 1-hour TTL for bots — they're not price-sensitive in real-time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Category pages&lt;/strong&gt;: 15-minute TTL for everyone — bots get the same cached content as humans&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grace period&lt;/strong&gt;: Old cached content served while fetching fresh data prevents backend spikes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Blocking Malicious Bots
&lt;/h2&gt;

&lt;p&gt;Not all bots are well-behaved. Some ignore &lt;code&gt;robots.txt&lt;/code&gt;, spoof user agents, and aggressively crawl your site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nginx Bad Bot Blocker
&lt;/h3&gt;

&lt;p&gt;Use the &lt;a href="https://github.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker" rel="noopener noreferrer"&gt;Nginx Bad Bot Blocker&lt;/a&gt; or implement a lightweight version:&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;# Block known bad user agents&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$http_user_agent&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;(ahrefsbot|semrushbot|mj12bot|dotbot|blexbot|ezooms|yandexbot|baiduspider|sogou|exabot|facebot|facebookexternalhit))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# Allow search engines, block SEO tools and scrapers&lt;/span&gt;
    &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$http_user_agent&lt;/span&gt; &lt;span class="s"&gt;!~*&lt;/span&gt; &lt;span class="s"&gt;(googlebot|bingbot|slurp|duckduckbot))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;444&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# Drop connection without response&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Block by IP reputation (maintain a deny list)&lt;/span&gt;
&lt;span class="k"&gt;include&lt;/span&gt; &lt;span class="n"&gt;/etc/nginx/bad_ips.conf&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fail2Ban Integration
&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/fail2ban/filter.d/magento-bad-bot.conf
&lt;/span&gt;&lt;span class="nn"&gt;[Definition]&lt;/span&gt;
&lt;span class="py"&gt;failregex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;^&amp;lt;HOST&amp;gt; .* "(GET|POST) .*" .* "(AhrefsBot|SemrushBot|MJ12bot|DotBot|BLEXBot)".*$&lt;/span&gt;
            &lt;span class="err"&gt;^&amp;lt;HOST&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;.*&lt;/span&gt; &lt;span class="err"&gt;"(GET|POST)&lt;/span&gt; &lt;span class="err"&gt;.*"&lt;/span&gt; &lt;span class="err"&gt;.*&lt;/span&gt; &lt;span class="err"&gt;HTTP/&lt;/span&gt;&lt;span class="nn"&gt;[0-9.]&lt;/span&gt;&lt;span class="err"&gt;+"&lt;/span&gt; &lt;span class="err"&gt;4&lt;/span&gt;&lt;span class="nn"&gt;[0-9]&lt;/span&gt;&lt;span class="err"&gt;{2}&lt;/span&gt; &lt;span class="err"&gt;.*&lt;/span&gt; &lt;span class="err"&gt;"(Python|curl|wget|Scrapy)".*$&lt;/span&gt;
&lt;span class="py"&gt;ignoreregex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;

&lt;span class="c"&gt;# /etc/fail2ban/jail.local
&lt;/span&gt;&lt;span class="nn"&gt;[magento-bad-bot]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;magento-bad-bot&lt;/span&gt;
&lt;span class="py"&gt;logpath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/nginx/access.log&lt;/span&gt;
&lt;span class="py"&gt;maxretry&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;findtime&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;bantime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3600&lt;/span&gt;
&lt;span class="py"&gt;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;iptables-multiport[name=BadBots, port="http,https", protocol=tcp]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Magento 2 Firewall Module
&lt;/h3&gt;

&lt;p&gt;For application-level blocking, use a module like &lt;code&gt;magento2-firewall&lt;/code&gt; or implement your own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Vendor\BotBlocker\Plugin&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\App\FrontControllerInterface&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\App\RequestInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Magento\Framework\HTTP\PhpEnvironment\Response&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;BotBlocker&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$blockedAgents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'AhrefsBot'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'SemrushBot'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'MJ12bot'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DotBot'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'BLEXBot'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Ezooms'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'curl'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wget'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'python-requests'&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;beforeDispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;FrontControllerInterface&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;RequestInterface&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="nv"&gt;$userAgent&lt;/span&gt; &lt;span class="o"&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;getHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'User-Agent'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;blockedAgents&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$bot&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="nb"&gt;stripos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$bot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setHttpResponseCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Forbidden'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sendResponse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;exit&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;h2&gt;
  
  
  Googlebot Crawl Budget Optimization
&lt;/h2&gt;

&lt;p&gt;Googlebot has a limited crawl budget for your site. Make sure it spends that budget on pages that matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Consolidate Pagination with &lt;code&gt;rel="canonical"&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- On page 2 of category listing --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://yourstore.com/electronics.html"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"prev"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://yourstore.com/electronics.html"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"next"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://yourstore.com/electronics.html?p=3"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Noindex Low-Value Filter Pages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- On layered navigation filter page with no products --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"robots"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"noindex, follow"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Proper Sitemap Management
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// In your sitemap generation cron or module&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Vendor\Sitemap\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;Magento\Sitemap\Model\Sitemap&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;OptimizedSitemap&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Sitemap&lt;/span&gt;
&lt;span class="p"&gt;{&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;_initSitemapItems&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&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;_initSitemapItems&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Exclude out-of-stock products from sitemap&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;_sitemapItems&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&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;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getUrl&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isOutOfStockProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getUrl&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;unset&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;_sitemapItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$key&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="c1"&gt;// Limit to 50,000 URLs per sitemap (Google's limit)&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;_sitemapItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_slice&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;_sitemapItems&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;50000&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;
  
  
  4. Monitor Google Search Console
&lt;/h3&gt;

&lt;p&gt;Check the "Crawl Stats" report weekly. Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Crawl rate drops&lt;/strong&gt; — Google might be hitting errors or timeouts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crawl budget waste&lt;/strong&gt; — pages that return 404 or are noindex being crawled repeatedly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server error spikes&lt;/strong&gt; — usually means bots are hitting broken URLs or overloading your infrastructure&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Track these metrics before and after implementing bot controls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cache hit ratio&lt;/td&gt;
&lt;td&gt;40–60%&lt;/td&gt;
&lt;td&gt;75–85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average response time&lt;/td&gt;
&lt;td&gt;800ms&lt;/td&gt;
&lt;td&gt;300ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP-FPM queue depth&lt;/td&gt;
&lt;td&gt;50+&lt;/td&gt;
&lt;td&gt;&amp;lt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL queries/sec&lt;/td&gt;
&lt;td&gt;5000+&lt;/td&gt;
&lt;td&gt;&amp;lt;2000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN egress (GB/day)&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bot requests/hour&lt;/td&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;td&gt;&amp;lt;5,000 (legitimate only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Quick Nginx Log Dashboard
&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# bot-impact.sh — run this hourly via cron&lt;/span&gt;

&lt;span class="nv"&gt;LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/log/nginx/access.log
&lt;span class="nv"&gt;HOUR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'1 hour ago'&lt;/span&gt; +%H&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Total requests in the last hour&lt;/span&gt;
&lt;span class="nv"&gt;TOTAL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;h&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOUR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'$4 ~ ":"h {count++} END {print count+0}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Bot requests&lt;/span&gt;
&lt;span class="nv"&gt;BOTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;h&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOUR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'$4 ~ ":"h &amp;amp;&amp;amp; /(Googlebot|bingbot|AhrefsBot|SemrushBot|MJ12bot)/ {count++} END {print count+0}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Cache misses (adjust based on your setup)&lt;/span&gt;
&lt;span class="nv"&gt;MISSES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;h&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOUR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'$4 ~ ":"h &amp;amp;&amp;amp; /MISS/ {count++} END {print count+0}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;PCT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&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;$TOTAL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &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="nv"&gt;PCT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;BOTS &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; TOTAL&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;: Total=&lt;/span&gt;&lt;span class="nv"&gt;$TOTAL&lt;/span&gt;&lt;span class="s2"&gt;, Bots=&lt;/span&gt;&lt;span class="nv"&gt;$BOTS&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PCT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%), Cache misses=&lt;/span&gt;&lt;span class="nv"&gt;$MISSES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;[ ] Implement a strict &lt;code&gt;robots.txt&lt;/code&gt; with layered navigation exclusions&lt;/li&gt;
&lt;li&gt;[ ] Set up Nginx rate limiting for both general traffic and bots&lt;/li&gt;
&lt;li&gt;[ ] Configure Varnish with bot-aware caching policies&lt;/li&gt;
&lt;li&gt;[ ] Deploy Fail2Ban to auto-block aggressive bots&lt;/li&gt;
&lt;li&gt;[ ] Add application-level bot logging and blocking&lt;/li&gt;
&lt;li&gt;[ ] Optimize sitemap.xml to guide crawlers efficiently&lt;/li&gt;
&lt;li&gt;[ ] Monitor Google Search Console crawl stats weekly&lt;/li&gt;
&lt;li&gt;[ ] Track cache hit ratio, response time, and server load metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Bot traffic is not going away. If anything, AI scrapers and SEO tools are becoming more aggressive. The good news is that most bot-related performance issues are solvable with the right combination of &lt;code&gt;robots.txt&lt;/code&gt; guidance, rate limiting, and caching strategy.&lt;/p&gt;

&lt;p&gt;The key is to be proactive. Don't wait for your server to crash during a crawler rampage. Implement these controls now, monitor your logs weekly, and adjust your defenses as bot behavior evolves. Your cache hit ratio, server costs, and sanity will thank you.&lt;/p&gt;

</description>
      <category>magento</category>
      <category>performance</category>
      <category>bots</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 UI Component Performance: Optimizing Admin Grids &amp; Data Providers</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Mon, 20 Jul 2026 09:07:56 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-ui-component-performance-optimizing-admin-grids-data-providers-2lo7</link>
      <guid>https://dev.to/magevanta/magento-2-ui-component-performance-optimizing-admin-grids-data-providers-2lo7</guid>
      <description>&lt;p&gt;Magento 2 UI components are the backbone of every admin grid you've ever used. The product grid. The order grid. The customer grid. The invoice grid. Every page in the admin panel that shows a table of data with filtering, sorting, and pagination is powered by the UI component system.&lt;/p&gt;

&lt;p&gt;And they are slow.&lt;/p&gt;

&lt;p&gt;Not "a little slow" — painfully slow on stores with 50,000+ products, 100,000+ orders, or multi-website setups with complex ACL. A product grid that takes 8 seconds to load is not uncommon. A customer grid that times out on filter? Also common. The irony is that the admin panel is where your team spends hours every day, and every second of grid load time is a second of lost productivity.&lt;/p&gt;

&lt;p&gt;This post covers why UI components are slow, how to profile them, and what you can do to make them fast again.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Magento 2 UI Components Work
&lt;/h2&gt;

&lt;p&gt;A UI component is a declarative XML configuration that Magento renders into a knockout.js-based frontend component. The typical flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;XML Declaration&lt;/strong&gt; — &lt;code&gt;view/adminhtml/ui_component/product_listing.xml&lt;/code&gt; defines columns, filters, bookmarks, and the data provider&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Provider&lt;/strong&gt; — A PHP class implementing &lt;code&gt;Magento\\Framework\\View\\Element\\UiComponent\\DataProvider\\DataProviderInterface&lt;/code&gt; that fetches data from the database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AJAX Endpoint&lt;/strong&gt; — The grid loads data via an AJAX call to &lt;code&gt;mui/index/render/&lt;/code&gt; which returns JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knockout.js Rendering&lt;/strong&gt; — The frontend renders the grid client-side using knockout templates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The performance problems live in steps 2 and 3. The data provider is where database queries run, and the AJAX endpoint is where Magento bootstraps the entire application just to serve a JSON response.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Performance Problems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Data Provider Collection Loading
&lt;/h3&gt;

&lt;p&gt;Every UI component grid uses a &lt;code&gt;Magento\\Framework\\View\\Element\\UiComponent\\DataProvider\\CollectionFactory&lt;/code&gt; to build a collection. For the product grid, this is &lt;code&gt;Magento\\Catalog\\Ui\\DataProvider\\Product\\ProductCollection&lt;/code&gt;. The collection loads products with EAV joins, inventory data, price indexing data, and store-specific attributes.&lt;/p&gt;

&lt;p&gt;The problem: the collection loads &lt;strong&gt;all EAV attributes&lt;/strong&gt; for every product in the grid, even though only a handful are displayed as columns. A product grid showing 20 products with 200 EAV attributes means 4,000 EAV joins — most of which are never rendered.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Full Page Bootstrap on Every AJAX Call
&lt;/h3&gt;

&lt;p&gt;When the grid requests data, it hits &lt;code&gt;mui/index/render/&lt;/code&gt;. This route bootstraps the entire Magento application: config loading, area code resolution, plugin interception, ACL checks, translation loading, layout generation. For a grid that paginates through 50 pages, that's 50 full Magento bootstraps — each one adding 200-500ms of overhead before the actual query runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Bookmark Management Overhead
&lt;/h3&gt;

&lt;p&gt;Magento saves grid state (filters, sorting, pagination) as "bookmarks" in the &lt;code&gt;ui_bookmark&lt;/code&gt; table. Every grid interaction — changing a filter, sorting a column, navigating pages — triggers a bookmark save operation. On multi-admin setups with many users, the bookmark table grows rapidly and queries against it slow down.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. AddColumn After Data Loading
&lt;/h3&gt;

&lt;p&gt;Many third-party modules add columns to grids via plugins on &lt;code&gt;getColumns()&lt;/code&gt; or by modifying the UI component XML. If a column requires additional data (e.g., a custom attribute from a third-party table), the data is often loaded per-row after the main collection has already been loaded. This is the classic N+1 problem: 20 products in a grid page means 20 additional queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Mass Action Preload
&lt;/h3&gt;

&lt;p&gt;When an admin user selects "Select All" in a grid, Magento needs to know all matching IDs — not just the current page. Some grids handle this by loading the full collection without LIMIT. On a store with 200,000 products, this means loading 200,000 product IDs into memory, serializing them, and sending them to the browser as a JavaScript variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profiling UI Component Performance
&lt;/h2&gt;

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

&lt;p&gt;Add this to &lt;code&gt;app/etc/env.php&lt;/code&gt; under the &lt;code&gt;db&lt;/code&gt; section:&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;'debug'&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="s1"&gt;'log_path'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/var/log/magento_db.log'&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;Load the product grid and check the log. You'll see dozens of queries per grid load — many of them EAV joins. Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queries with &lt;code&gt;JOIN ...catalog_product_entity_varchar&lt;/code&gt; repeated dozens of times&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COUNT(*)&lt;/code&gt; queries that scan the full product table&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SELECT * FROM ui_bookmark&lt;/code&gt; queries that are slower than expected&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'system'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'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;'enable'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Append &lt;code&gt;?profiler=1&lt;/code&gt; to any admin grid URL. Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Magento\\Framework\\View\\Element\\UiComponent\\DataProvider\\DataProvider&lt;/code&gt; — time spent in data provider&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Magento\\Ui\\Model\\Bookmark&lt;/code&gt; — bookmark management overhead&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Magento\\Framework\\Api\\SearchCriteriaBuilder&lt;/code&gt; — search criteria processing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Check AJAX Response Time
&lt;/h3&gt;

&lt;p&gt;Open Chrome DevTools → Network tab. Load the product grid. Look for the &lt;code&gt;mui/index/render/&lt;/code&gt; request. The total time is your grid load time. If it's over 2 seconds, you have a problem. If it's over 5 seconds, you have a serious problem.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  1. Limit EAV Attributes in the Collection
&lt;/h3&gt;

&lt;p&gt;The most impactful optimization is limiting which EAV attributes the product grid collection loads. By default, Magento loads all attributes that are configured as "Used in Grid" (&lt;code&gt;is_used_in_grid = 1&lt;/code&gt; in &lt;code&gt;catalog_eav_attribute&lt;/code&gt;). Many of these are unnecessary.&lt;/p&gt;

&lt;p&gt;Check which attributes are flagged for grid use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;attribute_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_used_in_grid&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;catalog_eav_attribute&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_used_in_grid&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;Disable grid loading for attributes that don't appear in the grid:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;catalog_eav_attribute&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;is_used_in_grid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;eav_attribute&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;attribute_code&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;'description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'short_description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'meta_title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'meta_description'&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 alone can reduce grid load time by 30-50% on stores with many EAV attributes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Optimize the Data Provider
&lt;/h3&gt;

&lt;p&gt;Create a custom data provider that extends the default one and optimizes the collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MageVanta\UiPerformance\Ui\DataProvider&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\Catalog\Ui\DataProvider\Product\ProductCollection&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\Catalog\Ui\DataProvider\Product\ProductDataProvider&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;OptimizedProductDataProvider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ProductDataProvider&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;getData&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 load attributes that are actually displayed as columns&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;getCollection&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;'sku'&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;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'visibility'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="c1"&gt;// Add index hints for large catalogs&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;getCollection&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;getSelect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Magento\Framework\DB\Select&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ORDER&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;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'entity_id DESC'&lt;/span&gt;&lt;span class="p"&gt;);&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;getData&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;Reference it in your &lt;code&gt;product_listing.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;dataSource&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"product_listing_data_source"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dataProvider&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"MageVanta\UiPerformance\Ui\DataProvider\OptimizedProductDataProvider"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"product_listing_data_source"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;settings&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;requestFieldName&amp;gt;&lt;/span&gt;id&lt;span class="nt"&gt;&amp;lt;/requestFieldName&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;primaryFieldName&amp;gt;&lt;/span&gt;entity_id&lt;span class="nt"&gt;&amp;lt;/primaryFieldName&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/settings&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dataProvider&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dataSource&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Add Database Indexes for Grid Filters
&lt;/h3&gt;

&lt;p&gt;The most common grid filters — status, visibility, type_id, attribute_set_id — need proper indexes. Check your slow query log for queries on &lt;code&gt;catalog_product_entity_int&lt;/code&gt; with filter conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Add composite index for common grid filter combinations&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_int&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_grid_filter&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Index for the entity_id join used in every grid load&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_int&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_entity_attribute&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attribute_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Implement Server-Side Pagination Correctly
&lt;/h3&gt;

&lt;p&gt;Magento's UI component system supports server-side pagination, but some custom grids break this by loading the full collection. Ensure your data provider respects the pagination parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$collection&lt;/span&gt; &lt;span class="o"&gt;=&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;getCollection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Respect page size from search criteria&lt;/span&gt;
    &lt;span class="nv"&gt;$searchCriteria&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSearchCriteria&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$collection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setPageSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$searchCriteria&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getPageSize&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nv"&gt;$collection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setCurPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$searchCriteria&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCurrentPage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$collection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getItems&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$items&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&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;'totalRecords'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$collection&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSize&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s1"&gt;'items'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$items&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 key insight: &lt;code&gt;$collection-&amp;gt;getSize()&lt;/code&gt; runs a &lt;code&gt;COUNT(*)&lt;/code&gt; query without loading all items, while &lt;code&gt;$collection-&amp;gt;getItems()&lt;/code&gt; only loads the current page. Never call &lt;code&gt;getItems()&lt;/code&gt; before setting the page size.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Cache Bookmark Queries
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;ui_bookmark&lt;/code&gt; table is queried on every grid render. Add a cache layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MageVanta\UiPerformance\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;Magento\Ui\Model\BookmarkRepository&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\App\CacheInterface&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;CachedBookmarkRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BookmarkRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;CACHE_KEY_PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ui_bookmark_'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;CACHE_TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$cache&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="kt"&gt;BookmarkRepository&lt;/span&gt; &lt;span class="nv"&gt;$subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;CacheInterface&lt;/span&gt; &lt;span class="nv"&gt;$cache&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;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$cache&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;getListByUserId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CACHE_KEY_PREFIX&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&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;getListByUserId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&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;$result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ui_bookmark'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CACHE_TTL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Replace the AJAX Endpoint with a Custom Route
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;mui/index/render/&lt;/code&gt; endpoint bootstraps the full Magento application on every request. For high-traffic admin panels, create a lightweight REST endpoint that skips layout generation and area code overhead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MageVanta\UiPerformance\Controller\Grid&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\App\Action\Action&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\App\Action\Context&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\App\ResponseInterface&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\Json\Helper\Data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;JsonHelper&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;MageVanta\UiPerformance\Model\GridDataResolver&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;Data&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Action&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$jsonHelper&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$gridDataResolver&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="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$namespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRequest&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;getParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'namespace'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;gridDataResolver&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$namespace&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;getRequest&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;getParams&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;jsonHelper&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;jsonEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;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 approach skips the layout XML processing that &lt;code&gt;mui/index/render/&lt;/code&gt; performs, saving 100-200ms per request.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Optimize Mass Action Handling
&lt;/h3&gt;

&lt;p&gt;Instead of loading all matching IDs into the browser, implement a server-side filter storage for mass actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRequest&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;getParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'filter'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$filterHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;md5&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;$filter&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Store filter criteria in a temporary table&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;filterStorage&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filterHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Return just the hash — not 200,000 IDs&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="nf"&gt;jsonResponse&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'filter_hash'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$filterHash&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;When the mass action is executed, the backend reads the filter from storage and applies it to the collection — no need to send all IDs to the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring UI Component Performance
&lt;/h2&gt;

&lt;p&gt;Add a plugin to &lt;code&gt;Magento\\Ui\\Controller\\Index\\Render&lt;/code&gt; to log grid load times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;aroundExecute&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;$proceed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$proceed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;microtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$duration&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'Slow UI component render: %s took %dms'&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;getRequest&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;getParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'namespace'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nv"&gt;$duration&lt;/span&gt;
        &lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Set up an alert for any grid load exceeding 3 seconds. On a well-optimized store, the product grid should load in under 1.5 seconds, and the order grid in under 1 second.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Magento 2 UI components are powerful but inefficient by default. The combination of EAV overhead, full application bootstrap on every AJAX call, and unoptimized data providers creates a perfect storm of admin panel slowness.&lt;/p&gt;

&lt;p&gt;The most impactful changes you can make:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reduce EAV attribute loading&lt;/strong&gt; — disable &lt;code&gt;is_used_in_grid&lt;/code&gt; for unused attributes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add proper database indexes&lt;/strong&gt; — on &lt;code&gt;catalog_product_entity_int&lt;/code&gt; for grid filter columns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache bookmark queries&lt;/strong&gt; — the &lt;code&gt;ui_bookmark&lt;/code&gt; table is hit on every grid render&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace the default AJAX endpoint&lt;/strong&gt; — skip layout generation for grid data requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement server-side mass action filters&lt;/strong&gt; — never send all IDs to the browser&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On a store with 100,000+ products, these optimizations typically reduce product grid load time from 6-8 seconds to under 2 seconds. That's a 4x improvement in admin panel responsiveness — and your team will feel it immediately.&lt;/p&gt;

</description>
      <category>magento</category>
    </item>
  </channel>
</rss>
