<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Magevanta</title>
    <description>The latest articles on DEV Community by Magevanta (@magevanta).</description>
    <link>https://dev.to/magevanta</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3887629%2F7af145fd-03e9-4362-99dc-a5f637f09ce1.png</url>
      <title>DEV Community: Magevanta</title>
      <link>https://dev.to/magevanta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/magevanta"/>
    <language>en</language>
    <item>
      <title>How to Reduce Magento 2 Bootstrap Time by 30%</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 19 Apr 2026 18:04:04 +0000</pubDate>
      <link>https://dev.to/magevanta/how-to-reduce-magento-2-bootstrap-time-by-30-aaf</link>
      <guid>https://dev.to/magevanta/how-to-reduce-magento-2-bootstrap-time-by-30-aaf</guid>
      <description>&lt;p&gt;Every time Magento handles a request, it loads hundreds of PHP classes, registers thousands of event observers, and wires up a plugin chain across the entire framework. Most of this work is for modules you never use.&lt;/p&gt;

&lt;p&gt;The fix is straightforward in theory: remove unused modules. In practice, it requires care — Magento's dependency graph is complex, and removing the wrong module can break things silently.&lt;/p&gt;

&lt;p&gt;Here's how to do it safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why module count matters
&lt;/h2&gt;

&lt;p&gt;A standard Magento 2.4.x installation ships with &lt;strong&gt;approximately 420 modules&lt;/strong&gt;. A typical B2C store actively uses 80–120 of them.&lt;/p&gt;

&lt;p&gt;The remaining 300 modules still have a cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Autoloader pressure&lt;/strong&gt; — PHP's autoloader registers class paths for every enabled module. More modules = more paths to check on every class load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observer overhead&lt;/strong&gt; — every enabled module can register event observers. Magento dispatches events on every request; each observer adds overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin chains&lt;/strong&gt; — interceptors (plugins) wrap core methods. Each plugin adds a function call. Deep plugin chains on hot code paths (like &lt;code&gt;Magento\Framework\App\Http::launch&lt;/code&gt;) measurably slow bootstrap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DI compilation&lt;/strong&gt; — more modules = larger generated DI configuration = slower &lt;code&gt;di:compile&lt;/code&gt; and more memory during compilation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which modules are safe to remove?
&lt;/h2&gt;

&lt;p&gt;Generally safe to disable on a standard B2C store:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B2B modules&lt;/strong&gt; (if you don't use B2B features):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Magento_Company&lt;/code&gt;, &lt;code&gt;Magento_SharedCatalog&lt;/code&gt;, &lt;code&gt;Magento_NegotiableQuote&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Magento_PurchaseOrder&lt;/code&gt;, &lt;code&gt;Magento_Requisition&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Unused payment methods:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any &lt;code&gt;Magento_Paypal*&lt;/code&gt;, &lt;code&gt;Magento_Braintree&lt;/code&gt;, &lt;code&gt;Magento_Authorizenet&lt;/code&gt; modules for providers you don't use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Unused shipping methods:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Magento_Fedex&lt;/code&gt;, &lt;code&gt;Magento_Ups&lt;/code&gt;, &lt;code&gt;Magento_Usps&lt;/code&gt;, &lt;code&gt;Magento_Dhl&lt;/code&gt; if you use a third-party shipping module&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sample data&lt;/strong&gt; (if installed):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Magento_CatalogSampleData&lt;/code&gt;, &lt;code&gt;Magento_CustomerSampleData&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Staging &amp;amp; Preview&lt;/strong&gt; (if not on Commerce):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All &lt;code&gt;Magento_*Staging&lt;/code&gt; modules&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The dependency problem
&lt;/h2&gt;

&lt;p&gt;You can't just disable a module — you need to check if anything depends on it first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check what depends on a module before disabling&lt;/span&gt;
bin/magento module:status &lt;span class="nt"&gt;--show-list&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"Magento_Braintree"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Disabling a module that something else depends on causes cryptic errors. Always check the full dependency tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-step: safe module removal
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Create a full backup first.&lt;/strong&gt; Always. No exceptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Analyze your current module set:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento module:status &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; modules-before.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Identify candidates:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Look for modules in &lt;code&gt;vendor/magento/&lt;/code&gt; that your store doesn't use&lt;/li&gt;
&lt;li&gt;Cross-reference with &lt;code&gt;composer.json&lt;/code&gt; to see which are explicitly required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Test in staging first:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento module:disable Magento_Braintree &lt;span class="nt"&gt;--clear-static-content&lt;/span&gt;
bin/magento setup:upgrade
bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Run your test suite.&lt;/strong&gt; If you don't have one, manually test checkout, catalog browsing, admin functions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Measure the difference:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before&lt;/span&gt;
&lt;span class="nb"&gt;time &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://your-store.com/ &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

&lt;span class="c"&gt;# After disabling modules&lt;/span&gt;
&lt;span class="nb"&gt;time &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://your-store.com/ &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. If all good, repeat in production.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Expected results
&lt;/h2&gt;

&lt;p&gt;Results vary by store configuration, but typical outcomes:&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 (30 modules removed)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bootstrap time&lt;/td&gt;
&lt;td&gt;180ms&lt;/td&gt;
&lt;td&gt;130ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP memory peak&lt;/td&gt;
&lt;td&gt;28MB&lt;/td&gt;
&lt;td&gt;22MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DI compile time&lt;/td&gt;
&lt;td&gt;4m 20s&lt;/td&gt;
&lt;td&gt;3m 10s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observer count&lt;/td&gt;
&lt;td&gt;1,840&lt;/td&gt;
&lt;td&gt;1,320&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 20–40% bootstrap improvement is realistic on most stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating this safely
&lt;/h2&gt;

&lt;p&gt;Manual module analysis is tedious and error-prone. The &lt;a href="https://magevanta.com/extensions" rel="noopener noreferrer"&gt;BetterMagento Turbo Core module&lt;/a&gt; provides an interactive CLI wizard that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Builds the complete dependency graph for your installation&lt;/li&gt;
&lt;li&gt;Identifies modules with no dependents that aren't actively used&lt;/li&gt;
&lt;li&gt;Shows you the safe removal candidates ranked by impact&lt;/li&gt;
&lt;li&gt;Lets you preview changes before applying them&lt;/li&gt;
&lt;li&gt;Generates a rollback script so you can undo any change
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Analyze your installation&lt;/span&gt;
bin/magento bm:turbo:analyze

&lt;span class="c"&gt;# Preview what would be removed&lt;/span&gt;
bin/magento bm:turbo:optimize &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

&lt;span class="c"&gt;# Apply with automatic rollback script generation&lt;/span&gt;
bin/magento bm:turbo:optimize &lt;span class="nt"&gt;--generate-rollback&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Important caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Always test in staging before production&lt;/li&gt;
&lt;li&gt;Re-run analysis after major Magento upgrades&lt;/li&gt;
&lt;li&gt;Some modules appear unused but are required by custom code — the analyzer checks for this&lt;/li&gt;
&lt;li&gt;Magento updates may re-enable disabled modules; check after upgrades&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The performance gains are real and compound with other optimizations. Combined with Redis caching, query optimization, and edge delivery, you can cut total request time by 60%+ on a well-tuned store.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://magevanta.com/blog/reduce-magento-2-bootstrap-time" rel="noopener noreferrer"&gt;magevanta.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Deployment: Zero-Downtime Production Deployments</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 19 Apr 2026 18:03:28 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-deployment-zero-downtime-production-deployments-22eo</link>
      <guid>https://dev.to/magevanta/magento-2-deployment-zero-downtime-production-deployments-22eo</guid>
      <description>&lt;p&gt;Magento deployments done wrong mean minutes of downtime, broken shopping carts, and unhappy customers. Done right, they're invisible — customers never notice. Here's how to get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Magento deployments are hard
&lt;/h2&gt;

&lt;p&gt;A typical Magento deployment touches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP files (application code)&lt;/li&gt;
&lt;li&gt;Generated code (&lt;code&gt;var/generation/&lt;/code&gt;, &lt;code&gt;generated/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Static files (&lt;code&gt;pub/static/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Database schema and data&lt;/li&gt;
&lt;li&gt;Cache (must be flushed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The challenge: each of these operations takes time, and during that time your store is in an inconsistent state. New code referencing old database schema. New static files not yet deployed. Old cached content serving.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive approach (and why it fails)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Bad: everything happens in-place&lt;/span&gt;
git pull origin main
composer &lt;span class="nb"&gt;install
&lt;/span&gt;php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento setup:static-content:deploy
php bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During &lt;code&gt;setup:di:compile&lt;/code&gt; (2–5 minutes), your store serves requests without compiled DI — intermittent 500 errors. During &lt;code&gt;setup:static-content:deploy&lt;/code&gt;, JS/CSS files may be missing. Classic downtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1: Maintenance mode (simple, has downtime)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php bin/magento maintenance:enable &lt;span class="nt"&gt;--ip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_IP

git pull origin main
composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--optimize-autoloader&lt;/span&gt;
php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento setup:static-content:deploy &lt;span class="nt"&gt;-f&lt;/span&gt; en_US
php bin/magento cache:flush

php bin/magento maintenance:disable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maintenance mode shows a "temporarily unavailable" page. Downtime: 3–10 minutes depending on catalog size. Acceptable for small stores, not for high-traffic production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 2: Atomic symlink deployment (zero downtime)
&lt;/h2&gt;

&lt;p&gt;The industry-standard approach: deploy to a new directory, then atomically switch the webserver symlink.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Directory structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/www/
├── releases/
│   ├── 20260419_143000/    ← current deployment
│   └── 20260419_120000/    ← previous deployment
├── shared/
│   ├── pub/media/          ← shared across deployments
│   ├── var/                ← shared var directory
│   └── app/etc/env.php     ← shared config
└── current -&amp;gt; releases/20260419_143000/  ← symlink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Deployment script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nv"&gt;RELEASE_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/www/releases/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d_%H%M%S&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;SHARED_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/www/shared"&lt;/span&gt;
&lt;span class="nv"&gt;CURRENT_LINK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/www/current"&lt;/span&gt;

&lt;span class="c"&gt;# 1. Create new release directory&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RELEASE_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
git clone &lt;span class="nt"&gt;--depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 git@github.com:yourorg/magento-store.git &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RELEASE_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RELEASE_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 2. Install dependencies&lt;/span&gt;
composer &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--optimize-autoloader&lt;/span&gt; &lt;span class="nt"&gt;--no-interaction&lt;/span&gt;

&lt;span class="c"&gt;# 3. Link shared directories&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; pub/media var app/etc/env.php
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SHARED_DIR&lt;/span&gt;&lt;span class="s2"&gt;/pub/media"&lt;/span&gt; pub/media
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SHARED_DIR&lt;/span&gt;&lt;span class="s2"&gt;/var"&lt;/span&gt; var
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SHARED_DIR&lt;/span&gt;&lt;span class="s2"&gt;/app/etc/env.php"&lt;/span&gt; app/etc/env.php

&lt;span class="c"&gt;# 4. Build static assets and DI (while old release is still live)&lt;/span&gt;
php bin/magento setup:di:compile
php bin/magento setup:static-content:deploy &lt;span class="nt"&gt;-f&lt;/span&gt; en_US de_DE

&lt;span class="c"&gt;# 5. Run database upgrades (safe to run before switch)&lt;/span&gt;
php bin/magento setup:upgrade &lt;span class="nt"&gt;--keep-generated&lt;/span&gt;

&lt;span class="c"&gt;# 6. ATOMIC SWITCH — this is instant&lt;/span&gt;
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sfn&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RELEASE_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_LINK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 7. Flush caches (new code is now live)&lt;/span&gt;
php bin/magento cache:flush

&lt;span class="c"&gt;# 8. Clean up old releases (keep last 3)&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; /var/www/releases | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; +4 | xargs &lt;span class="nt"&gt;-I&lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"/var/www/releases/{}"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Deployment complete: &lt;/span&gt;&lt;span class="nv"&gt;$RELEASE_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is step 6: &lt;code&gt;ln -sfn&lt;/code&gt; is an atomic operation on Linux. The webserver symlink switches from old to new in a single filesystem operation — no window where the symlink is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 3: Blue-green deployment
&lt;/h2&gt;

&lt;p&gt;For maximum safety: run two identical environments (blue and green), load balancer switches between them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Load Balancer
├── Blue environment (magento-blue.internal)  ← currently live
└── Green environment (magento-green.internal) ← deploy here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy to green while blue serves traffic&lt;/li&gt;
&lt;li&gt;Run smoke tests on green&lt;/li&gt;
&lt;li&gt;Switch load balancer to green (instant, no downtime)&lt;/li&gt;
&lt;li&gt;Blue becomes the new staging environment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Works best with containerized deployments (Docker/Kubernetes) or if your hosting provider supports it (AWS, GCP, Azure).&lt;/p&gt;

&lt;h2&gt;
  
  
  Database migrations without downtime
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;setup:upgrade&lt;/code&gt; runs database migrations. Running migrations while the store is live risks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Old code reading new schema columns that don't exist yet&lt;/li&gt;
&lt;li&gt;New code failing on old schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Backward-compatible migration pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Phase 1 (current deployment): add new column as nullable with default&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;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;new_field&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Phase 2 (next deployment): make the column required, backfill data&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;sales_order&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;new_field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'default_value'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;new_field&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;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;MODIFY&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;new_field&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Phase 3 (later): remove old compatibility code&lt;/p&gt;

&lt;p&gt;This "expand and contract" pattern lets you run migrations without the risk of old code breaking on new schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD pipeline example (GitHub Actions)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to Production&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy via SSH&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@master&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PROD_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SSH_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;cd /var/www&lt;/span&gt;
            &lt;span class="s"&gt;./scripts/deploy.sh&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Smoke test&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;sleep 10&lt;/span&gt;
          &lt;span class="s"&gt;curl -f https://your-store.com/ || exit 1&lt;/span&gt;
          &lt;span class="s"&gt;curl -f https://your-store.com/catalog/product/view/id/1 || exit 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Post-deployment checklist
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify indexers aren't invalid&lt;/span&gt;
bin/magento indexer:status

&lt;span class="c"&gt;# Verify caches are warmed&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code} %{time_total}s"&lt;/span&gt; https://your-store.com/

&lt;span class="c"&gt;# Check error logs&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt; var/log/system.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; error
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt; var/log/exception.log

&lt;span class="c"&gt;# Verify static files deployed&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://your-store.com/static/frontend/Magento/luma/en_US/requirejs/require.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero-downtime deployments require upfront setup but pay off immediately — no more stress about deployment windows, no more customer complaints during releases.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://magevanta.com/blog/magento-2-zero-downtime-deployment" rel="noopener noreferrer"&gt;magevanta.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Varnish Configuration: The Production-Ready Setup</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 19 Apr 2026 17:55:22 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-varnish-configuration-the-production-ready-setup-4i8b</link>
      <guid>https://dev.to/magevanta/magento-2-varnish-configuration-the-production-ready-setup-4i8b</guid>
      <description>&lt;p&gt;Varnish is the gold standard for full-page caching in front of Magento. Configured correctly, it serves the vast majority of your traffic without touching PHP at all — response times under 5ms. Configured incorrectly, it creates subtle bugs and a false sense of security.&lt;/p&gt;

&lt;p&gt;Here's the production-ready setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Varnish over Magento's built-in FPC
&lt;/h2&gt;

&lt;p&gt;Magento ships with a built-in full page cache (backed by filesystem or Redis). Varnish is better for high-traffic stores because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It sits in front of PHP entirely&lt;/strong&gt; — Varnish serves cached responses without starting PHP-FPM at all&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Massive throughput&lt;/strong&gt; — Varnish can serve tens of thousands of requests per second on modest hardware&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ESI support&lt;/strong&gt; — Edge Side Includes let you cache most of a page while keeping dynamic fragments fresh&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sophisticated TTL control&lt;/strong&gt; — per-URL, per-content-type, per-header rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For stores under ~10k daily visits, Redis FPC is often sufficient. Above that, Varnish pays for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing and wiring up Varnish
&lt;/h2&gt;

&lt;p&gt;Install Varnish 7.x on Ubuntu/Debian:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;varnish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure Varnish to listen on port 80, proxy to your web server on port 8080:&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="o"&gt;/&lt;/span&gt;etc&lt;span class="o"&gt;/&lt;/span&gt;varnish&lt;span class="o"&gt;/&lt;/span&gt;default.vcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tell Magento to use Varnish:&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 system/full_page_cache/caching_application 2
bin/magento config:set system/full_page_cache/varnish/access_list &lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;
bin/magento config:set system/full_page_cache/varnish/backend_host &lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;
bin/magento config:set system/full_page_cache/varnish/backend_port &lt;span class="s2"&gt;"8080"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate the Magento-recommended VCL:&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 varnish:vcl:generate &lt;span class="nt"&gt;--export-version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/varnish/default.vcl
service varnish restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The critical VCL settings most guides miss
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Normalize Accept-Encoding headers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without this, Varnish caches separate entries for gzip, deflate, and uncompressed responses — tripling your cache size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vcl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub&lt;/span&gt; &lt;span class="nf"&gt;vcl_recv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.http.Accept-Encoding&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;req.url&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s2"&gt;"\.(jpg|jpeg|png|gif|gz|tgz|bz2|ico|mp4|ogg|swf|flv)$"&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.Accept-Encoding&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.http.Accept-Encoding&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s2"&gt;"gzip"&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.Accept-Encoding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gzip"&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;unset&lt;/span&gt; &lt;span class="nv"&gt;req.http.Accept-Encoding&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;2. Strip marketing query parameters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;?utm_source=google&amp;amp;utm_campaign=spring&lt;/code&gt; creates separate cache entries for every ad campaign click. Strip them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vcl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub&lt;/span&gt; &lt;span class="nf"&gt;vcl_recv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;req.url&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.url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"[?&amp;amp;](utm_source|utm_medium|utm_campaign|utm_content|gclid|fbclid)=[^&amp;amp;]+"&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.url&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.url&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="s2"&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;&lt;strong&gt;3. Never cache logged-in sessions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Magento sets a &lt;code&gt;X-Magento-Vary&lt;/code&gt; cookie for logged-in users. Ensure Varnish passes these through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vcl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub&lt;/span&gt; &lt;span class="nf"&gt;vcl_recv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.http.cookie&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s2"&gt;"PHPSESSID"&lt;/span&gt; &lt;span class="o"&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;"frontend"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ESI for dynamic content
&lt;/h2&gt;

&lt;p&gt;ESI (Edge Side Includes) lets you cache 95% of a page in Varnish while keeping specific fragments dynamic — like the cart count, customer name, or "recently viewed" widget.&lt;/p&gt;

&lt;p&gt;In your layout XML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;block&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"Magento\PageCache\Block\Esi"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"cart.count"&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;"handles"&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;"default"&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;default&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;argument&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"ttl"&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;600&lt;span class="nt"&gt;&amp;lt;/argument&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/arguments&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/block&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Varnish assembles the final page by fetching the ESI fragment separately (served from a shorter-TTL cache or PHP directly).&lt;/p&gt;

&lt;p&gt;Enable ESI in Varnish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vcl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub&lt;/span&gt; &lt;span class="nf"&gt;vcl_backend_response&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.do_esi&lt;/span&gt; &lt;span class="o"&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;h2&gt;
  
  
  Cache purging on content updates
&lt;/h2&gt;

&lt;p&gt;When a product is updated, Magento sends a &lt;code&gt;BAN&lt;/code&gt; request to Varnish to invalidate affected cache entries. Your VCL must handle this:&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;acl&lt;/span&gt; purge &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;sub&lt;/span&gt; &lt;span class="nf"&gt;vcl_recv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"PURGE"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;client.ip&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; purge&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;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Not allowed"&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;purge&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;req.method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"BAN"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;client.ip&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; purge&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;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Not allowed"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;ban&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"obj.http.X-Magento-Tags ~ "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;req.http.X-Magento-Tags-Pattern&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;synth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Banned"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This enables Magento's tag-based Varnish purging — only pages containing the updated product/category are invalidated, not your entire cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring Varnish
&lt;/h2&gt;

&lt;p&gt;Key metrics to watch:&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;# Overall hit rate&lt;/span&gt;
varnishstat &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"MAIN.cache_hit"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"MAIN.cache_miss"&lt;/span&gt;

&lt;span class="c"&gt;# Current request rate&lt;/span&gt;
varnishstat &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"MAIN.client_req"&lt;/span&gt;

&lt;span class="c"&gt;# Live request log&lt;/span&gt;
varnishlog &lt;span class="nt"&gt;-i&lt;/span&gt; ReqURL &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"ReqMethod eq 'GET'"&lt;/span&gt;

&lt;span class="c"&gt;# Objects in cache&lt;/span&gt;
varnishstat &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"MAIN.n_object"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Target hit rate:&lt;/strong&gt; &amp;gt; 85% for anonymous traffic. If you're below this, check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Are you stripping UTM parameters? (cache fragmentation)&lt;/li&gt;
&lt;li&gt;Are logged-in user requests being passed correctly?&lt;/li&gt;
&lt;li&gt;Are your TTLs too short?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Not stripping UTM params&lt;/td&gt;
&lt;td&gt;Low hit rate&lt;/td&gt;
&lt;td&gt;Add regex filter in vcl_recv&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caching 404 pages&lt;/td&gt;
&lt;td&gt;Cache pollution&lt;/td&gt;
&lt;td&gt;Set short TTL for non-2xx responses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not normalizing Accept-Encoding&lt;/td&gt;
&lt;td&gt;3× cache size&lt;/td&gt;
&lt;td&gt;Strip and re-set header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing BAN ACL&lt;/td&gt;
&lt;td&gt;Cache never purges&lt;/td&gt;
&lt;td&gt;Add purge ACL and handler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESI not enabled&lt;/td&gt;
&lt;td&gt;Dynamic content cached&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;beresp.do_esi = true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Varnish + tag-based purging + ESI for dynamic fragments is the fastest Magento caching architecture available. Get the configuration right and you'll serve millions of requests without breaking a sweat.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://magevanta.com/blog/magento-2-varnish-configuration-guide" rel="noopener noreferrer"&gt;magevanta.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Slow Queries: How to Find and Fix Them</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 19 Apr 2026 17:54:46 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-slow-queries-how-to-find-and-fix-them-3oke</link>
      <guid>https://dev.to/magevanta/magento-2-slow-queries-how-to-find-and-fix-them-3oke</guid>
      <description>&lt;p&gt;If your Magento store is slow, the database is almost always the first place to look. A single unoptimized query running on every page load can add hundreds of milliseconds to your response time — and on a store with thousands of products, slow queries are almost inevitable.&lt;/p&gt;

&lt;p&gt;Here's the complete playbook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Enable the slow query log
&lt;/h2&gt;

&lt;p&gt;MySQL and MariaDB have a built-in slow query log. Enable it on your server:&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;slow_query_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ON'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;long_query_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;slow_query_log_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/var/log/mysql/slow-queries.log'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;log_queries_not_using_indexes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ON'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;long_query_time&lt;/code&gt; to 1 second as a starting threshold. Once you've fixed the worst offenders, lower it to 0.5 or even 0.1.&lt;/p&gt;

&lt;p&gt;For permanent configuration, add to &lt;code&gt;my.cnf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[mysqld]&lt;/span&gt;
&lt;span class="py"&gt;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-queries.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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Analyze the slow query log
&lt;/h2&gt;

&lt;p&gt;After collecting a few hours of data, use &lt;code&gt;mysqldumpslow&lt;/code&gt; to summarize:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysqldumpslow &lt;span class="nt"&gt;-s&lt;/span&gt; t &lt;span class="nt"&gt;-t&lt;/span&gt; 20 /var/log/mysql/slow-queries.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows your 20 slowest queries sorted by total execution time. You'll typically find a handful of queries responsible for 80%+ of your database load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Run EXPLAIN on slow queries
&lt;/h2&gt;

&lt;p&gt;For each slow query, run &lt;code&gt;EXPLAIN&lt;/code&gt; to understand what MySQL is doing:&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;a1&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="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;a1&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;a1&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;a1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attribute_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;73&lt;/span&gt;
&lt;span class="k"&gt;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="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key things to look for in the output:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Bad sign&lt;/th&gt;
&lt;th&gt;Good sign&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ALL&lt;/code&gt; (full table scan)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;eq_ref&lt;/code&gt;, &lt;code&gt;const&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NULL&lt;/code&gt; (no index used)&lt;/td&gt;
&lt;td&gt;Index name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;High number&lt;/td&gt;
&lt;td&gt;Low number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Extra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Using filesort&lt;/code&gt;, &lt;code&gt;Using temporary&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Using index&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A &lt;code&gt;type: ALL&lt;/code&gt; with &lt;code&gt;rows: 500000&lt;/code&gt; means MySQL is scanning your entire products table for every request. That's a critical fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Add missing indexes
&lt;/h2&gt;

&lt;p&gt;Once you've identified a query doing a full table scan, check if an index would help:&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;catalog_product_entity_varchar&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Add an index if missing&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_varchar&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; Adding indexes on large tables takes time and locks the table. Use &lt;code&gt;ALTER TABLE ... ALGORITHM=INPLACE, LOCK=NONE&lt;/code&gt; on MySQL 5.7+ or run during a maintenance window.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: Common Magento slow query patterns
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;EAV overload — too many attribute joins:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Magento's EAV architecture means product data is spread across multiple tables. A single product load can generate 20–30 joins. The fix: use flat catalog tables or selectively index frequently-accessed attributes.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Unindexed product collection filters:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Custom collection filters on non-indexed attributes cause full scans. Always check if the attribute has &lt;code&gt;Used in Search&lt;/code&gt; or a proper index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ORDER BY on non-indexed columns:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sorting on columns without indexes causes &lt;code&gt;Using filesort&lt;/code&gt; — MySQL sorts in memory or on disk instead of using 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;-- Add a composite index for common sort patterns&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_created_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="n"&gt;status&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;N+1 queries in loops:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad: 1 query per product&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;$products&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;$category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCategory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// new query each iteration&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Good: load all at once&lt;/span&gt;
&lt;span class="nv"&gt;$productIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$products&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getAllIds&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$categoryRepository&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getListByProductIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$productIds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Query result caching
&lt;/h2&gt;

&lt;p&gt;Some queries are inherently expensive but don't change often — complex reports, faceted navigation counts, bestseller calculations. Cache their results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'category_product_count_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$categoryId&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$result&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;runExpensiveQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$categoryId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$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;'category_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$categoryId&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Automating the process
&lt;/h2&gt;

&lt;p&gt;Manual slow query analysis works but doesn't scale. The &lt;a href="https://magevanta.com/extensions" rel="noopener noreferrer"&gt;BetterMagento Query Optimizer&lt;/a&gt; automates the entire workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Continuously monitors your slow query log&lt;/li&gt;
&lt;li&gt;Runs EXPLAIN analysis automatically&lt;/li&gt;
&lt;li&gt;Suggests specific indexes with the exact &lt;code&gt;ALTER TABLE&lt;/code&gt; statements to run&lt;/li&gt;
&lt;li&gt;Tracks query performance over time&lt;/li&gt;
&lt;li&gt;Alerts you when new slow queries appear&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most stores, it finds 5–15 high-impact indexes that can be added in under an hour — often cutting total database query time by 40–60%.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;[ ] Enable slow query log (threshold: 1s)&lt;/li&gt;
&lt;li&gt;[ ] Analyze with &lt;code&gt;mysqldumpslow&lt;/code&gt; weekly&lt;/li&gt;
&lt;li&gt;[ ] Run EXPLAIN on top 10 slow queries&lt;/li&gt;
&lt;li&gt;[ ] Add missing indexes&lt;/li&gt;
&lt;li&gt;[ ] Enable flat catalog tables&lt;/li&gt;
&lt;li&gt;[ ] Cache results of expensive but stable queries&lt;/li&gt;
&lt;li&gt;[ ] Set up automated monitoring&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://magevanta.com/blog/magento-2-slow-queries-fix" rel="noopener noreferrer"&gt;magevanta.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Security Hardening: A Production Checklist for 2026</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 19 Apr 2026 17:49:00 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-security-hardening-a-production-checklist-for-2026-28n2</link>
      <guid>https://dev.to/magevanta/magento-2-security-hardening-a-production-checklist-for-2026-28n2</guid>
      <description>&lt;p&gt;Magento stores are high-value targets. They process payments, store customer data, and often run on shared infrastructure. A compromised store means payment card theft, data breaches, and regulatory fines.&lt;/p&gt;

&lt;p&gt;This checklist covers the most impactful security hardening steps for a production Magento 2 store.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Change the admin URL
&lt;/h2&gt;

&lt;p&gt;The default admin URL &lt;code&gt;/admin&lt;/code&gt; is targeted by automated scanners within hours of a store going live. Change it to something non-obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento setup:config:set &lt;span class="nt"&gt;--backend-frontname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_secret_admin_path"&lt;/span&gt;
bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or 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;'backend'&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;'frontName'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'your_secret_path'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use something random and non-guessable. Not &lt;code&gt;/manager&lt;/code&gt;, &lt;code&gt;/backend&lt;/code&gt;, or &lt;code&gt;/store-admin&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Enable two-factor authentication
&lt;/h2&gt;

&lt;p&gt;Magento 2.4+ ships with 2FA built in. Enable it and enforce it for all admin users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento module:enable Magento_TwoFactorAuth
bin/magento setup:upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported authenticators: Google Authenticator, Authy, Duo Security, U2F.&lt;/p&gt;

&lt;p&gt;For headless admin access (API), whitelist specific IPs instead of disabling 2FA:&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 twofactorauth/general/force_providers &lt;span class="s2"&gt;"google"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. File permissions
&lt;/h2&gt;

&lt;p&gt;Magento's recommended file permissions:&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;# Directories&lt;/span&gt;
find var generated vendor pub/static pub/media app/etc &lt;span class="nt"&gt;-type&lt;/span&gt; d &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;770 &lt;span class="o"&gt;{}&lt;/span&gt; +

&lt;span class="c"&gt;# Files  &lt;/span&gt;
find var generated vendor pub/static pub/media app/etc &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;660 &lt;span class="o"&gt;{}&lt;/span&gt; +

&lt;span class="c"&gt;# bin/magento&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;770 bin/magento

&lt;span class="c"&gt;# Never allow write on app/code&lt;/span&gt;
find app/code &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;640 &lt;span class="o"&gt;{}&lt;/span&gt; +
find app/code &lt;span class="nt"&gt;-type&lt;/span&gt; d &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;750 &lt;span class="o"&gt;{}&lt;/span&gt; +
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Critical: &lt;code&gt;app/etc/env.php&lt;/code&gt; contains database credentials. Ensure it's not world-readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;640 app/etc/env.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Disable directory listing
&lt;/h2&gt;

&lt;p&gt;In your nginx config:&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;autoindex&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or in Apache &lt;code&gt;.htaccess&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;Options&lt;/span&gt; -Indexes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Directory listing exposes your file structure to attackers.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Content Security Policy headers
&lt;/h2&gt;

&lt;p&gt;Magento 2.4+ has CSP support. Enable report-only mode first to find violations without breaking your store:&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 csp/mode/storefront/report_only 1
bin/magento config:set csp/mode/admin/report_only 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure your CSP whitelist in Admin → Security → Content Security Policy. Add your analytics, payment processor, and CDN domains.&lt;/p&gt;

&lt;p&gt;Once violations are resolved, switch to enforce mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento config:set csp/mode/storefront/report_only 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Security headers
&lt;/h2&gt;

&lt;p&gt;Add these to your nginx config:&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;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"SAMEORIGIN"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&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="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Referrer-Policy&lt;/span&gt; &lt;span class="s"&gt;"strict-origin-when-cross-origin"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Permissions-Policy&lt;/span&gt; &lt;span class="s"&gt;"camera=(),&lt;/span&gt; &lt;span class="s"&gt;microphone=(),&lt;/span&gt; &lt;span class="s"&gt;geolocation=()"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Only once CSP is configured&lt;/span&gt;
&lt;span class="c1"&gt;# add_header Content-Security-Policy "..." always;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Keep Magento patched
&lt;/h2&gt;

&lt;p&gt;Magento releases security patches regularly. Subscribe to the Magento security alert RSS feed and the Adobe security bulletin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check current version&lt;/span&gt;
bin/magento &lt;span class="nt"&gt;--version&lt;/span&gt;

&lt;span class="c"&gt;# Check for available updates&lt;/span&gt;
composer outdated magento/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply security patches within 2 weeks of release. Critical patches within 48 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Restrict admin access by IP
&lt;/h2&gt;

&lt;p&gt;If your team accesses admin from predictable IPs, whitelist them at the nginx level:&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="s"&gt;^/your_admin_path&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;allow&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="s"&gt;.3.4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;# office IP&lt;/span&gt;
    &lt;span class="kn"&gt;allow&lt;/span&gt; &lt;span class="mf"&gt;5.6&lt;/span&gt;&lt;span class="s"&gt;.7.8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;# developer IP&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This stops brute-force admin attacks even if the attacker knows your admin URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Disable unused payment methods and modules
&lt;/h2&gt;

&lt;p&gt;Every enabled payment method is potential attack surface. Disable anything you don't use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/magento module:disable Magento_Paypal
bin/magento module:disable Magento_Braintree
&lt;span class="c"&gt;# etc.&lt;/span&gt;
bin/magento setup:upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  10. Monitor for malware
&lt;/h2&gt;

&lt;p&gt;Magento stores are targeted by card-skimming malware (Magecart attacks). Malware is typically injected into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript files in &lt;code&gt;pub/static/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PHP files in &lt;code&gt;app/code/&lt;/code&gt; or templates&lt;/li&gt;
&lt;li&gt;Database (inline scripts in CMS blocks or product descriptions)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set up file integrity monitoring:&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;# Create a baseline hash of all PHP files&lt;/span&gt;
find app/code vendor/magento &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.php"&lt;/span&gt; &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;md5sum&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/integrity/baseline.txt

&lt;span class="c"&gt;# Run daily and alert on changes&lt;/span&gt;
find app/code vendor/magento &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.php"&lt;/span&gt; &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;md5sum&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt; | diff /var/integrity/baseline.txt - | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"^&amp;gt;"&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For database-injected malware, scan CMS blocks and product descriptions weekly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cms_block&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%&amp;lt;script%'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%requirejs%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  11. Secure env.php
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;app/etc/env.php&lt;/code&gt; contains your database password, Redis password, and encryption key. Never commit it to version control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"app/etc/env.php"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use environment variables or a secrets manager for CI/CD pipelines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws secretsmanager get-secret-value &lt;span class="nt"&gt;--secret-id&lt;/span&gt; prod/magento/db &lt;span class="nt"&gt;--query&lt;/span&gt; SecretString &lt;span class="nt"&gt;--output&lt;/span&gt; text | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .password&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Security audit checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Admin URL changed from &lt;code&gt;/admin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] 2FA enabled for all admin users&lt;/li&gt;
&lt;li&gt;[ ] File permissions set correctly&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;env.php&lt;/code&gt; not world-readable, not in git&lt;/li&gt;
&lt;li&gt;[ ] Directory listing disabled&lt;/li&gt;
&lt;li&gt;[ ] Security headers configured (X-Frame-Options, X-Content-Type)&lt;/li&gt;
&lt;li&gt;[ ] CSP in report-only mode (working toward enforce)&lt;/li&gt;
&lt;li&gt;[ ] Admin IP restriction (if team has static IPs)&lt;/li&gt;
&lt;li&gt;[ ] Unused modules and payment methods disabled&lt;/li&gt;
&lt;li&gt;[ ] File integrity monitoring running&lt;/li&gt;
&lt;li&gt;[ ] On latest Magento security patch&lt;/li&gt;
&lt;li&gt;[ ] SSL/TLS 1.3 enabled, TLS 1.0/1.1 disabled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Security is never "done" — it's an ongoing process. Schedule a quarterly security review against this checklist.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://magevanta.com/blog/magento-2-security-hardening-checklist" rel="noopener noreferrer"&gt;magevanta.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Redis Configuration: The Complete Guide for 2026</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 19 Apr 2026 17:48:24 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-redis-configuration-the-complete-guide-for-2026-26pn</link>
      <guid>https://dev.to/magevanta/magento-2-redis-configuration-the-complete-guide-for-2026-26pn</guid>
      <description>&lt;p&gt;Redis is table stakes for any production Magento store. But "just install Redis" is not enough — the default configuration leaves significant performance on the table. Here's how to configure it correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why separate Redis instances matter
&lt;/h2&gt;

&lt;p&gt;Many guides tell you to point all Magento caches at a single Redis instance. Don't do this.&lt;/p&gt;

&lt;p&gt;Magento uses Redis for three distinct workloads:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Default cache&lt;/strong&gt; — config, layouts, translations (many small, short-lived keys)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Page Cache (FPC)&lt;/strong&gt; — rendered HTML pages (large values, longer TTL)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sessions&lt;/strong&gt; — customer session data (medium values, must persist)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mixing these in one instance creates problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FPC's large HTML values evict small config cache entries under memory pressure&lt;/li&gt;
&lt;li&gt;A cache flush (&lt;code&gt;cache:flush&lt;/code&gt;) wipes sessions if they share the same database&lt;/li&gt;
&lt;li&gt;You can't tune eviction policy per workload&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The right setup: 3 separate Redis instances&lt;/strong&gt; (or at minimum, separate databases on one instance with different &lt;code&gt;maxmemory&lt;/code&gt; settings).&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;env.php&lt;/code&gt; configuration
&lt;/h2&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'cache'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'frontend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'id_prefix'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'mage_'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'backend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Magento\\Framework\\Cache\\Backend\\Redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'backend_options'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="s1"&gt;'server'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6379'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'compress_data'&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;'compression_lib'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'lzf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'page_cache'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'id_prefix'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'mage_fpc_'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'backend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Magento\\Framework\\Cache\\Backend\\Redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'backend_options'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="s1"&gt;'server'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// separate DB&lt;/span&gt;
                    &lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6379'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'compress_data'&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;'compression_lib'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'lzf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'session'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'save'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'redis'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'host'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6379'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2.5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'persistent_identifier'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'database'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// separate DB&lt;/span&gt;
            &lt;span class="s1"&gt;'compression_threshold'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2048'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'compression_library'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'lzf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'log_level'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'max_concurrency'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'6'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'break_after_frontend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'break_after_adminhtml'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'30'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'first_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'600'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'bot_first_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'60'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'bot_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'7200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'disable_locking'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'min_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'60'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'max_lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2592000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;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;
  
  
  Redis server configuration
&lt;/h2&gt;

&lt;p&gt;Tune your Redis &lt;code&gt;redis.conf&lt;/code&gt; per workload:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For default cache (DB 0) — aggressive eviction, it's rebuildable:&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;maxmemory&lt;/span&gt; &lt;span class="m"&gt;512&lt;/span&gt;&lt;span class="n"&gt;mb&lt;/span&gt;
&lt;span class="n"&gt;maxmemory&lt;/span&gt;-&lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="n"&gt;allkeys&lt;/span&gt;-&lt;span class="n"&gt;lru&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For full page cache (DB 1) — larger allocation, LRU eviction:&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;maxmemory&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="n"&gt;gb&lt;/span&gt;
&lt;span class="n"&gt;maxmemory&lt;/span&gt;-&lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="n"&gt;allkeys&lt;/span&gt;-&lt;span class="n"&gt;lru&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For sessions (DB 2) — no eviction, sessions must persist:&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;maxmemory&lt;/span&gt; &lt;span class="m"&gt;256&lt;/span&gt;&lt;span class="n"&gt;mb&lt;/span&gt;
&lt;span class="n"&gt;maxmemory&lt;/span&gt;-&lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="n"&gt;noeviction&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;noeviction&lt;/code&gt; policy for sessions is critical. Without it, Redis can evict session keys under memory pressure — silently logging customers out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connection pooling with PHP-FPM
&lt;/h2&gt;

&lt;p&gt;Each PHP-FPM worker opens its own connection to Redis. With 100 workers and 3 Redis connections each, you're looking at 300 simultaneous connections.&lt;/p&gt;

&lt;p&gt;Mitigate this with &lt;code&gt;persistent_identifier&lt;/code&gt; in your session config and connection pooling via Twemproxy or Redis Cluster if you're at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compression settings
&lt;/h2&gt;

&lt;p&gt;Enabling compression reduces memory usage by 40–60% for typical Magento cache values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lzf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Very fast&lt;/td&gt;
&lt;td&gt;~2x&lt;/td&gt;
&lt;td&gt;Default cache, sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lz4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extremely fast&lt;/td&gt;
&lt;td&gt;~2x&lt;/td&gt;
&lt;td&gt;High-throughput FPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zstd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;~3-4x&lt;/td&gt;
&lt;td&gt;Large HTML pages in FPC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Set &lt;code&gt;compression_threshold&lt;/code&gt; to 2048 bytes — don't compress small values, the overhead isn't worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring Redis health
&lt;/h2&gt;

&lt;p&gt;Check these regularly:&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;# Overall stats&lt;/span&gt;
redis-cli info stats

&lt;span class="c"&gt;# Memory usage&lt;/span&gt;
redis-cli info memory | &lt;span class="nb"&gt;grep &lt;/span&gt;used_memory_human

&lt;span class="c"&gt;# Hit rate (aim for &amp;gt; 95%)&lt;/span&gt;
redis-cli info stats | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"keyspace_hits|keyspace_misses"&lt;/span&gt;

&lt;span class="c"&gt;# Top 10 slowest commands&lt;/span&gt;
redis-cli slowlog get 10

&lt;span class="c"&gt;# Keys by database&lt;/span&gt;
redis-cli info keyspace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Hit rate formula:&lt;/strong&gt; &lt;code&gt;hits / (hits + misses) * 100&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If your hit rate drops below 80%, you either have a memory pressure problem (increase &lt;code&gt;maxmemory&lt;/code&gt;) or a cache invalidation problem (too aggressive flushing).&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache warming after deploys
&lt;/h2&gt;

&lt;p&gt;After a full cache flush (e.g. post-deploy), your store will be slow until Redis warms back up. Automate warming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# warm-cache.sh — crawl top pages after deploy&lt;/span&gt;
&lt;span class="nv"&gt;urls&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
  &lt;span class="s2"&gt;"https://your-store.com/"&lt;/span&gt;
  &lt;span class="s2"&gt;"https://your-store.com/catalog/category/view/id/5"&lt;/span&gt;
  &lt;span class="s2"&gt;"https://your-store.com/catalog/product/view/id/100"&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;url &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&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;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Warmed: &lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://magevanta.com/extensions" rel="noopener noreferrer"&gt;BetterMagento Redis Turbo module&lt;/a&gt; automates cache warming, monitors hit rates in the admin panel, and provides tag-based invalidation so you never have to flush the entire cache again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick reference: common mistakes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single Redis instance for all caches&lt;/td&gt;
&lt;td&gt;Use separate databases or instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No compression&lt;/td&gt;
&lt;td&gt;Enable &lt;code&gt;lzf&lt;/code&gt; compression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;noeviction&lt;/code&gt; on cache (not sessions)&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;allkeys-lru&lt;/code&gt; for cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions sharing DB with cache&lt;/td&gt;
&lt;td&gt;Separate DB, &lt;code&gt;noeviction&lt;/code&gt; policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No monitoring&lt;/td&gt;
&lt;td&gt;Track hit rate, memory, slowlog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full flush on every deploy&lt;/td&gt;
&lt;td&gt;Use tag-based invalidation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://magevanta.com/blog/magento-2-redis-configuration-guide" rel="noopener noreferrer"&gt;magevanta.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Redis Caching for Magento 2: Beyond the Basics</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 19 Apr 2026 17:42:37 +0000</pubDate>
      <link>https://dev.to/magevanta/redis-caching-for-magento-2-beyond-the-basics-43le</link>
      <guid>https://dev.to/magevanta/redis-caching-for-magento-2-beyond-the-basics-43le</guid>
      <description>&lt;p&gt;Redis is the standard cache backend for production Magento stores. But most setups barely scratch the surface of what Redis can do — and pay the price in unnecessary cache flushes, slow bulk writes, and missing monitoring.&lt;/p&gt;

&lt;p&gt;Here's how to do it right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with default Magento Redis config
&lt;/h2&gt;

&lt;p&gt;Magento's built-in Redis integration works. But it has significant limitations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full cache flushes on every save.&lt;/strong&gt; By default, Magento flushes the entire cache when a product is saved. On a store with 100k products, that means your entire page cache is cold after every catalog update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No pipeline batching.&lt;/strong&gt; Each cache read/write is a separate TCP round-trip to Redis. For operations that set 50+ cache keys at once (like loading a category page), this adds up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No cluster support.&lt;/strong&gt; Magento's Redis adapter supports Sentinel (failover) but not Redis Cluster (horizontal scaling). On high-traffic stores, a single Redis instance becomes a bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tag-based cache invalidation
&lt;/h2&gt;

&lt;p&gt;The fix for full cache flushes: tag every cache entry with the entities it depends on.&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;// Store cache entry with tags&lt;/span&gt;
&lt;span class="nv"&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;$data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'product_detail_123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'product_123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'category_5'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Later: flush only entries tagged with product_123&lt;/span&gt;
&lt;span class="nv"&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="n"&gt;\Zend_Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CLEANING_MODE_MATCHING_TAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'product_123'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When product 123 is updated, only the pages that display that product are invalidated — not your entire cache.&lt;/p&gt;

&lt;p&gt;The challenge: Magento's cache adapter doesn't support this natively. You need a custom adapter that maintains a tag-to-key mapping in Redis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected improvement:&lt;/strong&gt; 90%+ reduction in cache misses after catalog updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pipeline batching
&lt;/h2&gt;

&lt;p&gt;Redis pipelines let you batch multiple commands into a single network round-trip:&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;// Without pipelining: 50 round-trips&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;$keys&lt;/span&gt; &lt;span class="k"&gt;as&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="nv"&gt;$redis&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&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="nv"&gt;$data&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="c1"&gt;// With pipelining: 1 round-trip&lt;/span&gt;
&lt;span class="nv"&gt;$pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$redis&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;pipeline&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;$keys&lt;/span&gt; &lt;span class="k"&gt;as&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="nv"&gt;$pipe&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&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="nv"&gt;$data&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="nv"&gt;$pipe&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For category pages that set 50–100 cache keys, pipelining reduces cache-write time from ~100ms to ~5ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redis Cluster vs. Sentinel
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sentinel&lt;/strong&gt; handles failover — if your primary Redis instance dies, Sentinel promotes a replica. You get high availability, but you're still limited to one primary node handling all writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redis Cluster&lt;/strong&gt; shards data across multiple nodes. You can scale writes horizontally by adding nodes. For stores doing millions of cache operations per hour, this matters.&lt;/p&gt;

&lt;p&gt;Configuration for Cluster in &lt;code&gt;env.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'cache'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'frontend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'backend'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'BetterMagento\RedisTurbo\Cache\Backend\RedisCluster'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'backend_options'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'cluster_nodes'&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;'redis-node-1:6379'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'redis-node-2:6379'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'redis-node-3:6379'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Session clustering
&lt;/h2&gt;

&lt;p&gt;Storing PHP sessions in Redis works well — but default configuration has a gotcha: if you have multiple app servers, a user's session can be on any node. If that node goes down, they're logged out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sticky sessions&lt;/strong&gt; solve this: route each user consistently to the same app server (and therefore the same Redis primary). Configure this at your load balancer level.&lt;/p&gt;

&lt;p&gt;For extra safety, enable session compression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'session'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'save'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'redis'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'compression_threshold'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// compress sessions &amp;gt; 2KB&lt;/span&gt;
        &lt;span class="s1"&gt;'compression_library'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'lzf'&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;
  
  
  Compression for large values
&lt;/h2&gt;

&lt;p&gt;Redis stores data in memory. Uncompressed cache values for full pages can be 50–200KB each. At scale, this adds up to gigabytes.&lt;/p&gt;

&lt;p&gt;Enable LZ4 or Zstandard compression for values above a threshold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LZ4: fastest compression, ~2x ratio — best for latency-sensitive caches&lt;/li&gt;
&lt;li&gt;Zstandard: slower compression, ~3–4x ratio — best for full page cache&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Expected improvement:&lt;/strong&gt; 30–50% reduction in Redis memory usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring
&lt;/h2&gt;

&lt;p&gt;You can't optimize what you don't measure. Key metrics to track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hit rate&lt;/strong&gt; — aim for &amp;gt;95% on page cache, &amp;gt;80% on default cache&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory usage&lt;/strong&gt; — alert at 80% of &lt;code&gt;maxmemory&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow commands&lt;/strong&gt; — &lt;code&gt;SLOWLOG GET 10&lt;/code&gt; in redis-cli&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eviction rate&lt;/strong&gt; — any evictions indicate memory pressure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://magevanta.com/extensions" rel="noopener noreferrer"&gt;BetterMagento Redis Turbo module&lt;/a&gt; ships with an admin dashboard showing all of these in real time.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full cache flushes&lt;/td&gt;
&lt;td&gt;Tag-based invalidation&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow bulk writes&lt;/td&gt;
&lt;td&gt;Pipeline batching&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-node bottleneck&lt;/td&gt;
&lt;td&gt;Redis Cluster&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High memory usage&lt;/td&gt;
&lt;td&gt;LZ4/Zstandard compression&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No visibility&lt;/td&gt;
&lt;td&gt;Admin monitoring dashboard&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Small config changes, massive performance gains.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://magevanta.com/blog/magento-2-redis-caching-advanced-guide" rel="noopener noreferrer"&gt;magevanta.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Performance Optimization: The Complete 2026 Guide</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 19 Apr 2026 17:42:01 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-performance-optimization-the-complete-2026-guide-3c3n</link>
      <guid>https://dev.to/magevanta/magento-2-performance-optimization-the-complete-2026-guide-3c3n</guid>
      <description>&lt;p&gt;Every second of load time costs you conversions. Studies consistently show a 1-second delay reduces conversions by 7%. For Magento stores, the challenge is real: Magento is a powerful platform, but out-of-the-box performance leaves room for improvement.&lt;/p&gt;

&lt;p&gt;This guide walks through the most impactful optimizations you can make in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Database Query Optimization
&lt;/h2&gt;

&lt;p&gt;The database is almost always the biggest bottleneck in a slow Magento store. Common culprits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;N+1 query problems&lt;/strong&gt; — loading related data in loops instead of joins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing indexes&lt;/strong&gt; — queries doing full table scans on large catalog tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unoptimized EAV queries&lt;/strong&gt; — Magento's EAV structure can generate hundreds of queries per page&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Enable the MySQL slow query log and set a threshold of 1–2 seconds. Analyze the output weekly. For any slow query, run &lt;code&gt;EXPLAIN&lt;/code&gt; to see if it's missing an index.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;SET GLOBAL slow_query_log &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ON'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
SET GLOBAL long_query_time &lt;span class="o"&gt;=&lt;/span&gt; 1&lt;span class="p"&gt;;&lt;/span&gt;
SET GLOBAL slow_query_log_file &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/var/log/mysql/slow.log'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tools like &lt;a href="https://magevanta.com/extensions" rel="noopener noreferrer"&gt;BetterMagento Query Optimizer&lt;/a&gt; can automate this — detecting slow queries, running EXPLAIN analysis, and suggesting indexes without manual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Redis Caching
&lt;/h2&gt;

&lt;p&gt;Magento's default cache backend (filesystem) doesn't scale. Redis is the standard for production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full Page Cache&lt;/strong&gt; — cache rendered HTML pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default cache&lt;/strong&gt; — config, layout, translations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session storage&lt;/strong&gt; — distributed sessions for multi-server setups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference in response times is dramatic: filesystem cache typically adds 50–200ms per request, Redis brings it under 5ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key configuration pitfalls:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use separate Redis instances for FPC and default cache&lt;/li&gt;
&lt;li&gt;Enable compression for large cache values&lt;/li&gt;
&lt;li&gt;Use tag-based invalidation — don't flush everything on every deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. API Performance for Headless Stores
&lt;/h2&gt;

&lt;p&gt;If you're running a headless frontend (React, Next.js, Vue), your GraphQL/REST API performance becomes critical.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persisted queries&lt;/strong&gt; — store query hashes server-side, send only the hash from the client&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response compression&lt;/strong&gt; — Brotli reduces payload size by 60–70% vs uncompressed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field pruning&lt;/strong&gt; — only return the fields the client actually requested&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; — protect your API from abuse without blocking legitimate traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Edge Delivery
&lt;/h2&gt;

&lt;p&gt;Traditional Magento runs everything on a single origin server. Edge delivery moves rendering to CDN edge nodes close to your users — cutting latency dramatically.&lt;/p&gt;

&lt;p&gt;For a user in Tokyo hitting a server in Frankfurt, latency alone adds 200–300ms per request. With edge delivery, that drops to under 50ms.&lt;/p&gt;

&lt;p&gt;Platforms like Cloudflare Workers and Fastly Compute@Edge support this model today.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Module Bloat
&lt;/h2&gt;

&lt;p&gt;A default Magento 2 installation ships with 400+ modules. Most stores use fewer than 100 of them. Every unused module adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP classes that must be autoloaded&lt;/li&gt;
&lt;li&gt;Observers that run on every event&lt;/li&gt;
&lt;li&gt;Plugins that wrap core methods&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Safely removing unused modules can reduce bootstrap time by 20–40%. Always use a tool that checks dependencies before removing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Core Web Vitals
&lt;/h2&gt;

&lt;p&gt;Google uses Core Web Vitals as a ranking signal. For Magento stores, the most impactful metrics are:&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;Target&lt;/th&gt;
&lt;th&gt;Common Magento Issue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;&amp;lt; 2.5s&lt;/td&gt;
&lt;td&gt;Unoptimized hero images, slow TTFB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FID/INP&lt;/td&gt;
&lt;td&gt;&amp;lt; 200ms&lt;/td&gt;
&lt;td&gt;Heavy JS bundles, third-party scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;&amp;lt; 0.1&lt;/td&gt;
&lt;td&gt;Missing image dimensions, late-loading fonts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Quick wins:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes to all images&lt;/li&gt;
&lt;li&gt;Preload your LCP image with &lt;code&gt;&amp;lt;link rel="preload"&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Defer non-critical JavaScript&lt;/li&gt;
&lt;li&gt;Use a CDN for static assets&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;Performance optimization is a process, not a one-time fix. The highest-impact stack for 2026:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Redis for all caching layers&lt;/li&gt;
&lt;li&gt;Tag-based cache invalidation (not full flushes)&lt;/li&gt;
&lt;li&gt;Database query monitoring with automated suggestions&lt;/li&gt;
&lt;li&gt;API compression and caching for headless deployments&lt;/li&gt;
&lt;li&gt;Edge delivery for global audiences&lt;/li&gt;
&lt;li&gt;Module audit to remove unused Magento core modules&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://magevanta.com/extensions" rel="noopener noreferrer"&gt;BetterMagento Suite&lt;/a&gt; covers all of these in one package.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://magevanta.com/blog/magento-2-performance-optimization-guide-2026" rel="noopener noreferrer"&gt;magevanta.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Module Development Best Practices in 2026</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 19 Apr 2026 17:36:14 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-module-development-best-practices-in-2026-35jo</link>
      <guid>https://dev.to/magevanta/magento-2-module-development-best-practices-in-2026-35jo</guid>
      <description>&lt;p&gt;Writing Magento 2 modules that don't break stores, don't slow down performance, and survive upgrades requires discipline. After working on production Magento codebases, here are the patterns that matter most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service contracts: the right way to expose functionality
&lt;/h2&gt;

&lt;p&gt;Never depend directly on concrete model classes. Always use interfaces (service contracts):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad: depends on a concrete class&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\Catalog\Model\Product&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="c1"&gt;// Good: depends on an interface&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\Catalog\Api\ProductRepositoryInterface&lt;/span&gt; &lt;span class="nv"&gt;$productRepository&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;Why it matters: concrete models can be replaced or overridden in customized Magento installations. Depending on interfaces makes your module resilient to those changes.&lt;/p&gt;

&lt;p&gt;If your module exposes its own functionality, define interfaces first:&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;// Api/MyModuleServiceInterface.php&lt;/span&gt;
&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;MyModuleServiceInterface&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;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$entityId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;ResultInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Model/MyModuleService.php&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyModuleService&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;MyModuleServiceInterface&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;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$entityId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;ResultInterface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Plugins vs. observers: choosing correctly
&lt;/h2&gt;

&lt;p&gt;Both plugins and observers let you hook into Magento's core behavior. Choosing wrong creates bugs and performance problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use observers when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're reacting to something that already happened (after event)&lt;/li&gt;
&lt;li&gt;Multiple modules might observe the same event (no conflict)&lt;/li&gt;
&lt;li&gt;You don't need to modify the return value
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Good observer usage: react to order placement&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Magento\Framework\Event\Observer&lt;/span&gt; &lt;span class="nv"&gt;$observer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$observer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getOrder&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;notificationService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;notifyNewOrder&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;getId&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;Use plugins when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to modify input parameters (before plugin)&lt;/li&gt;
&lt;li&gt;You need to modify the return value (after plugin)&lt;/li&gt;
&lt;li&gt;You need to wrap execution (around plugin)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Good plugin usage: add data to a repository result&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;afterGetById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;ProductRepositoryInterface&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;ProductInterface&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$productId&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;ProductInterface&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;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setCustomData&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;getCustomData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$productId&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;&lt;strong&gt;Avoid around plugins unless necessary.&lt;/strong&gt; Around plugins intercept the entire method call and are significantly slower than before/after plugins. They also make debugging harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency injection done right
&lt;/h2&gt;

&lt;p&gt;Magento's DI system is powerful but easy to misuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use constructor injection for required dependencies:&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;LoggerInterface&lt;/span&gt; &lt;span class="nv"&gt;$logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;CacheInterface&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="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;ProductRepositoryInterface&lt;/span&gt; &lt;span class="nv"&gt;$productRepository&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;Use factories for objects you create on-the-fly:&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nc"&gt;\Magento\Catalog\Model\ProductFactory&lt;/span&gt; &lt;span class="nv"&gt;$productFactory&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;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;createProduct&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Product&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;productFactory&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inject the ObjectManager directly: never.&lt;/strong&gt; It bypasses DI, makes testing impossible, and creates hidden dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance-conscious coding
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Avoid loading product/order collections in loops:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bad: O(n) queries&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;$orderIds&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$orderId&lt;/span&gt;&lt;span class="p"&gt;)&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;=&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;orderRepository&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// query per iteration&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;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Good: load all at once&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="n"&gt;searchCriteriaBuilder&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'entity_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$orderIds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'in'&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;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$orders&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;orderRepository&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$searchCriteria&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$orders&lt;/span&gt;&lt;span class="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;$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="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cache expensive operations:&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getExpensiveData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'my_module_data_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$id&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="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cached&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$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="nf"&gt;runExpensiveCalculation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&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;'my_module_tag'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$data&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;Use indexers for frequently-queried derived data:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're computing something on every page load that could be pre-computed (like product scores, custom pricing, or aggregated stats), build an indexer. Compute once, read many times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing your module
&lt;/h2&gt;

&lt;p&gt;Untested Magento modules break in production. At minimum:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unit tests&lt;/strong&gt; for business logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyServiceTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&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;testProcessReturnsSuccessResult&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;$service&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;MyService&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;createMock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LoggerInterface&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createMock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CacheInterface&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="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;$service&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;process&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertTrue&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;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isSuccess&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;Integration tests&lt;/strong&gt; for database operations and DI-heavy 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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyRepositoryTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;\Magento\TestFramework\TestCase\AbstractController&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;testSaveAndLoad&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;$repository&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;_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;MyRepositoryInterface&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="c1"&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;Aim for &amp;gt;60% test coverage on your business logic. Models and repositories are the most important to test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema and data patches over upgrade scripts
&lt;/h2&gt;

&lt;p&gt;Old-style &lt;code&gt;UpgradeSchema.php&lt;/code&gt; and &lt;code&gt;UpgradeData.php&lt;/code&gt; scripts are deprecated. Use declarative schema and data patches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- etc/db_schema.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"my_module_entity"&lt;/span&gt; &lt;span class="na"&gt;resource=&lt;/span&gt;&lt;span class="s"&gt;"default"&lt;/span&gt; &lt;span class="na"&gt;engine=&lt;/span&gt;&lt;span class="s"&gt;"innodb"&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;"entity_id"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"int"&lt;/span&gt; &lt;span class="na"&gt;unsigned=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;nullable=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="na"&gt;identity=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;column&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;"varchar"&lt;/span&gt; &lt;span class="na"&gt;length=&lt;/span&gt;&lt;span class="s"&gt;"255"&lt;/span&gt; &lt;span class="na"&gt;nullable=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"created_at"&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"timestamp"&lt;/span&gt; &lt;span class="na"&gt;default=&lt;/span&gt;&lt;span class="s"&gt;"CURRENT_TIMESTAMP"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;constraint&lt;/span&gt; &lt;span class="na"&gt;xsi:type=&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt; &lt;span class="na"&gt;referenceId=&lt;/span&gt;&lt;span class="s"&gt;"PRIMARY"&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;"entity_id"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/constraint&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/table&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="c1"&gt;// Setup/Patch/Data/AddDefaultConfiguration.php&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddDefaultConfiguration&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;DataPatchInterface&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;apply&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;configWriter&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="s1"&gt;'my_module/general/enabled'&lt;/span&gt;&lt;span class="p"&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getDependencies&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="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;getAliases&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="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;Patches run once and are tracked in &lt;code&gt;patch_list&lt;/code&gt; table. No more version comparison logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Module structure that scales
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/code/Vendor/ModuleName/
├── Api/                    # Service contract interfaces
│   └── Data/               # Data object interfaces
├── Block/                  # View models and blocks
├── Console/                # CLI commands
├── Controller/             # HTTP controllers
├── Cron/                   # Scheduled jobs
├── Model/                  # Business logic implementations
│   └── ResourceModel/      # Database layer
├── Observer/               # Event observers
├── Plugin/                 # Interceptors
├── Setup/                  # Schema + data patches
├── Test/                   # Unit + integration tests
├── Ui/                     # UI components
├── etc/                    # Configuration
│   ├── adminhtml/          # Admin-specific config
│   ├── frontend/           # Frontend-specific config
│   ├── db_schema.xml       # Declarative schema
│   ├── di.xml              # DI configuration
│   ├── events.xml          # Event observers
│   └── module.xml          # Module declaration
├── view/                   # Templates, layouts, JS, CSS
├── composer.json
└── registration.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure is predictable for any Magento developer joining your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Staying upgrade-safe
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prefer &lt;code&gt;preference&lt;/code&gt; in &lt;code&gt;di.xml&lt;/code&gt; as a last resort; use plugins when possible&lt;/li&gt;
&lt;li&gt;Don't override core templates unless absolutely necessary&lt;/li&gt;
&lt;li&gt;Test against the next minor Magento version in CI before it's released&lt;/li&gt;
&lt;li&gt;Follow Magento's deprecation notices in each release's changelogs&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://magevanta.com/blog/magento-2-module-development-best-practices" rel="noopener noreferrer"&gt;magevanta.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Indexer Optimization: Faster Reindexing in Production</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 19 Apr 2026 17:35:38 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-indexer-optimization-faster-reindexing-in-production-5350</link>
      <guid>https://dev.to/magevanta/magento-2-indexer-optimization-faster-reindexing-in-production-5350</guid>
      <description>&lt;p&gt;Magento's indexing system is one of the most misunderstood parts of the platform. Run it wrong and you get store lockups, slow admin saves, and customers seeing stale prices. Run it right and indexing becomes invisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What indexers actually do
&lt;/h2&gt;

&lt;p&gt;Magento uses indexers to pre-compute expensive derived data and store it in flat tables optimized for reads:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Indexer&lt;/th&gt;
&lt;th&gt;What it computes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalog_product_flat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Flat product data (faster reads than EAV)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalog_category_flat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Flat category data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalog_category_product&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Category-to-product mappings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cataloginventory_stock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stock status and availability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalog_product_price&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computed prices (after discounts, tier pricing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalogsearch_fulltext&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search index (Elasticsearch/OpenSearch)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;catalogrule_rule&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Catalog price rule applications&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When an indexer is "invalid," Magento serves stale data from the last valid index. When it's "processing," database tables are locked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scheduled vs. real-time indexing
&lt;/h2&gt;

&lt;p&gt;Each indexer can run in two modes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time (&lt;code&gt;realtime&lt;/code&gt;):&lt;/strong&gt; Reindexes automatically when data changes. Safe for small catalogs, dangerous for large ones — a single product save can trigger minutes of reindexing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scheduled (&lt;code&gt;schedule&lt;/code&gt;):&lt;/strong&gt; Queues changes and processes them in batches via cron. Much better for production.&lt;/p&gt;

&lt;p&gt;Switch all indexers to scheduled:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Verify:&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:info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With scheduled indexing, product saves complete instantly. The cron job processes the queue periodically (every minute by default).&lt;/p&gt;

&lt;h2&gt;
  
  
  The indexer cron configuration
&lt;/h2&gt;

&lt;p&gt;Magento's default cron runs indexers every minute. For large catalogs, this can cause cron jobs to overlap.&lt;/p&gt;

&lt;p&gt;Check your cron is running:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;For high-traffic stores, run indexers during off-peak hours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/code/YourVendor/CronOverride/etc/crontab.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;config&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;group&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"index"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;job&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"indexer_reindex_all_invalid"&lt;/span&gt; &lt;span class="na"&gt;instance=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Indexer\Cron\ReindexAllInvalid"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"execute"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;schedule&amp;gt;&lt;/span&gt;*/5 * * * *&lt;span class="nt"&gt;&amp;lt;/schedule&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;&amp;lt;!-- Every 5 minutes instead of every 1 --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/job&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/group&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Partial reindexing
&lt;/h2&gt;

&lt;p&gt;Running a full reindex on a store with millions of products takes hours. Partial reindexing processes only the changed rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Full reindex (avoid in production during business hours)&lt;/span&gt;
bin/magento indexer:reindex

&lt;span class="c"&gt;# Check which indexers need reindexing&lt;/span&gt;
bin/magento indexer:status

&lt;span class="c"&gt;# Reindex only a specific indexer&lt;/span&gt;
bin/magento indexer:reindex catalog_product_price
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With scheduled mode, partial reindexing happens automatically. The &lt;code&gt;mview&lt;/code&gt; (materialized view) system tracks changed rows in changelog tables and only processes those.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosing slow reindexing
&lt;/h2&gt;

&lt;p&gt;If your indexers consistently take too long, the culprits are usually:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Catalog price rule complexity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Complex pricing rules with many conditions force Magento to evaluate every product against every rule. Simplify rules where possible, or use customer group pricing instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Large EAV tables without indexes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The indexer reads from EAV tables. Missing database indexes on these tables make full reindexes extremely slow.&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 if these indexes exist&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;catalog_product_entity_decimal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- If missing, add them&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;catalog_product_entity_decimal&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IDX_ATTRIBUTE&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;p&gt;&lt;strong&gt;3. Elasticsearch document size&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;catalogsearch_fulltext&lt;/code&gt; indexer pushes data to Elasticsearch/OpenSearch. If you're indexing too many attributes, documents get large and indexing slows down.&lt;/p&gt;

&lt;p&gt;Only index attributes that users actually search on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Admin → Stores → Attributes → Product → [attribute] → Use in Search → Yes/No
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turn off "Use in Search" for attributes like internal SKUs, supplier codes, and technical specs that customers never search for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring indexer health
&lt;/h2&gt;

&lt;p&gt;Add indexer status to your monitoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# check-indexers.sh&lt;/span&gt;
&lt;span class="nv"&gt;STATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;bin/magento indexer:status | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Reindex required"&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;$STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ALERT: &lt;/span&gt;&lt;span class="nv"&gt;$STATUS&lt;/span&gt;&lt;span class="s2"&gt; indexers require reindexing"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"All indexers valid"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this in your monitoring cron every 30 minutes. Alert if any indexer has been invalid for &amp;gt; 15 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before a major import
&lt;/h2&gt;

&lt;p&gt;When you're importing thousands of products (e.g., catalog update from ERP), disable observers and run a full reindex 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;# Before import&lt;/span&gt;
bin/magento indexer:set-mode realtime cataloginventory_stock  &lt;span class="c"&gt;# Keep stock live&lt;/span&gt;
bin/magento indexer:set-mode schedule catalog_product_flat catalog_category_flat catalog_product_price catalogsearch_fulltext

&lt;span class="c"&gt;# Run import&lt;/span&gt;
bin/magento import:run ...

&lt;span class="c"&gt;# After import - full reindex of affected indexers&lt;/span&gt;
bin/magento indexer:reindex catalog_product_flat catalog_category_flat catalog_product_price catalogsearch_fulltext

&lt;span class="c"&gt;# Flush caches&lt;/span&gt;
bin/magento cache:flush
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern keeps your store responsive during the import and runs the reindex as a single batch operation after.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Switch all indexers to &lt;code&gt;schedule&lt;/code&gt; mode in production&lt;/li&gt;
&lt;li&gt;Ensure cron is running and not overlapping&lt;/li&gt;
&lt;li&gt;Monitor indexer status — alert on invalid indexers &amp;gt; 15 minutes&lt;/li&gt;
&lt;li&gt;Add database indexes to EAV tables to speed up full reindexes&lt;/li&gt;
&lt;li&gt;Only index attributes in Elasticsearch that users actually search on&lt;/li&gt;
&lt;li&gt;Batch large imports and reindex after, not during&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://magevanta.com/blog/magento-2-indexer-optimization" rel="noopener noreferrer"&gt;magevanta.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Magento 2 Headless Architecture: Performance Best Practices</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 19 Apr 2026 17:29:51 +0000</pubDate>
      <link>https://dev.to/magevanta/magento-2-headless-architecture-performance-best-practices-4e1</link>
      <guid>https://dev.to/magevanta/magento-2-headless-architecture-performance-best-practices-4e1</guid>
      <description>&lt;p&gt;Headless Magento gives you the freedom to build a blazing-fast frontend — but only if your API layer is architected correctly. A poorly configured GraphQL endpoint will make your headless store slower than a traditional Magento theme.&lt;/p&gt;

&lt;p&gt;Here's how to get it right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headless performance stack
&lt;/h2&gt;

&lt;p&gt;A well-architected headless Magento setup has three layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser / App
     ↓
CDN (Cloudflare / Fastly)          ← Cache static &amp;amp; public API responses
     ↓
Next.js / Nuxt / Astro frontend    ← SSR + ISR for dynamic content
     ↓
Magento GraphQL API                ← With Redis caching, compression, rate limiting
     ↓
MySQL + Redis                      ← Optimized queries, tag-based cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer needs to be optimized. Most guides only cover the frontend. Let's talk about the API layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  GraphQL vs REST for headless
&lt;/h2&gt;

&lt;p&gt;For product catalog, category navigation, and CMS content: &lt;strong&gt;use GraphQL&lt;/strong&gt;. It gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request exactly the fields you need (no over-fetching)&lt;/li&gt;
&lt;li&gt;Batch multiple queries in one request&lt;/li&gt;
&lt;li&gt;Strong typing helps frontend developers catch issues early&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For cart, checkout, and customer operations: &lt;strong&gt;REST is often faster&lt;/strong&gt; for simple operations (add to cart, apply coupon) because Magento's REST endpoints are more mature and have better caching support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-Side Rendering vs Static Generation
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page type&lt;/th&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Cache TTL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Homepage&lt;/td&gt;
&lt;td&gt;ISR (revalidate every 5 min)&lt;/td&gt;
&lt;td&gt;CDN: 5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Category pages&lt;/td&gt;
&lt;td&gt;ISR (revalidate every 10 min)&lt;/td&gt;
&lt;td&gt;CDN: 10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product detail&lt;/td&gt;
&lt;td&gt;ISR (revalidate on price/stock change)&lt;/td&gt;
&lt;td&gt;CDN: 1 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cart / Checkout&lt;/td&gt;
&lt;td&gt;SSR (no cache)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CMS pages&lt;/td&gt;
&lt;td&gt;Static (rebuild on publish)&lt;/td&gt;
&lt;td&gt;CDN: 24h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key insight: most of your traffic hits product and category pages. These can be statically generated or ISR-cached. Only cart and checkout need true SSR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching GraphQL responses at the edge
&lt;/h2&gt;

&lt;p&gt;Public GraphQL queries (product listings, category trees, CMS blocks) can be cached at the CDN edge:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy:&lt;/strong&gt; Convert POST GraphQL requests to GET using persisted queries, then cache the GET responses at CDN level.&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;// Next.js example: fetch with cache&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://shop.example.com/graphql&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&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;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PRODUCTS_QUERY&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// ISR: revalidate every 5 minutes&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Cloudflare, you can also cache GraphQL responses at the edge using Cache Rules based on query hash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invalidating the CDN on catalog updates
&lt;/h2&gt;

&lt;p&gt;The hardest problem in headless Magento: when a product price changes, you need to invalidate the right CDN pages without a full cache flush.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tag-based invalidation workflow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Product is saved in Magento admin&lt;/li&gt;
&lt;li&gt;Magento fires &lt;code&gt;catalog_product_save_after&lt;/code&gt; event&lt;/li&gt;
&lt;li&gt;Observer collects affected URLs (PDP, parent category pages, related widgets)&lt;/li&gt;
&lt;li&gt;Calls Cloudflare API to purge those specific URLs
&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Magento\Framework\Event\Observer&lt;/span&gt; &lt;span class="nv"&gt;$observer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$observer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getProduct&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$urls&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;urlCollector&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getAffectedUrls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cloudflareClient&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;purgePaths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$urls&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is more complex to set up but essential at scale. Flushing your entire CDN on every product save destroys your cache hit rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling customer-specific content
&lt;/h2&gt;

&lt;p&gt;Personalized content (prices for logged-in customers, wishlist counts, cart totals) can't be cached. But you don't have to sacrifice performance:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shell + client-side hydration pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Serve a cached shell (header, navigation, product grid) from CDN&lt;/li&gt;
&lt;li&gt;On load, fetch customer-specific data from a dedicated API endpoint&lt;/li&gt;
&lt;li&gt;Hydrate the shell with customer data client-side&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gives you CDN-fast page loads with accurate customer data. The perceived performance is excellent because the page structure appears immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  API rate limiting and abuse protection
&lt;/h2&gt;

&lt;p&gt;Headless APIs are more exposed than traditional Magento installs. Without rate limiting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Crawlers can saturate your GraphQL endpoint&lt;/li&gt;
&lt;li&gt;Competitors can scrape your entire catalog&lt;/li&gt;
&lt;li&gt;Checkout bots can abuse your cart API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Implement tiered rate limiting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unauthenticated requests: 60/minute per IP&lt;/li&gt;
&lt;li&gt;Authenticated frontend: 600/minute per session token&lt;/li&gt;
&lt;li&gt;Server-to-server (your Next.js backend): unlimited with a dedicated API key&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Monitoring API performance
&lt;/h2&gt;

&lt;p&gt;Track these metrics per endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;P50/P95/P99 response times&lt;/strong&gt; — P95 and P99 reveal tail latency that real users experience&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache hit rate&lt;/strong&gt; — aim for &amp;gt;90% on product/category endpoints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error rate&lt;/strong&gt; — alert at &amp;gt;0.1%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload size&lt;/strong&gt; — track average response size, flag regressions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 200ms P95 response time on your product API translates directly to a 200ms longer page load for 1 in 20 users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complete headless optimization checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Separate Redis instances for cache, FPC, and sessions&lt;/li&gt;
&lt;li&gt;[ ] GraphQL response caching in Redis (by query hash + store + currency)&lt;/li&gt;
&lt;li&gt;[ ] Brotli compression on all API responses&lt;/li&gt;
&lt;li&gt;[ ] Persisted queries for CDN cacheability&lt;/li&gt;
&lt;li&gt;[ ] Tag-based CDN invalidation on catalog updates&lt;/li&gt;
&lt;li&gt;[ ] Rate limiting with tiered API keys&lt;/li&gt;
&lt;li&gt;[ ] Shell + hydration pattern for customer-specific content&lt;/li&gt;
&lt;li&gt;[ ] P95 response time monitoring with alerts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://magevanta.com/extensions" rel="noopener noreferrer"&gt;BetterMagento Lightweight API module&lt;/a&gt; handles the Redis caching, Brotli compression, persisted queries, and rate limiting out of the box.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://magevanta.com/blog/magento-2-headless-performance-best-practices" rel="noopener noreferrer"&gt;magevanta.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>php</category>
      <category>performance</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>How to Speed Up Magento 2 GraphQL: A Developer's Guide</title>
      <dc:creator>Magevanta</dc:creator>
      <pubDate>Sun, 19 Apr 2026 17:29:15 +0000</pubDate>
      <link>https://dev.to/magevanta/how-to-speed-up-magento-2-graphql-a-developers-guide-55fm</link>
      <guid>https://dev.to/magevanta/how-to-speed-up-magento-2-graphql-a-developers-guide-55fm</guid>
      <description>&lt;p&gt;Magento 2's GraphQL API is powerful, but it's slow by default. Out of the box, every GraphQL request hits your PHP stack, executes database queries, and returns uncompressed JSON. For a headless storefront, this adds up fast.&lt;/p&gt;

&lt;p&gt;Here's how to make it production-fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Magento GraphQL is slow by default
&lt;/h2&gt;

&lt;p&gt;A few root causes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No response caching&lt;/strong&gt; — each request is processed from scratch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No compression&lt;/strong&gt; — JSON responses are sent uncompressed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-fetching&lt;/strong&gt; — clients receive all fields, even unused ones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rate limiting&lt;/strong&gt; — one misbehaving client can saturate your server&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's fix each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Response Caching
&lt;/h2&gt;

&lt;p&gt;The most impactful change. For public data (product listings, category pages, CMS blocks), GraphQL responses can be cached in Redis and reused across requests.&lt;/p&gt;

&lt;p&gt;Cache key strategy matters here: you need to vary the cache by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The query hash&lt;/li&gt;
&lt;li&gt;The store view&lt;/li&gt;
&lt;li&gt;The customer group (for pricing)&lt;/li&gt;
&lt;li&gt;Currency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tag your cache entries by entity type (&lt;code&gt;product_1234&lt;/code&gt;, &lt;code&gt;category_56&lt;/code&gt;) so you can invalidate precisely when data changes — not flush everything.&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;# Example: flush cache for a specific product&lt;/span&gt;
bin/magento cache:clean &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;full_page &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;product_1234
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected improvement:&lt;/strong&gt; 80–95% of GraphQL requests served from cache, response time drops from 200–500ms to under 10ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Brotli Compression
&lt;/h2&gt;

&lt;p&gt;Brotli compresses 15–25% better than gzip. For GraphQL responses (JSON), real-world compression ratios are typically 70–80%.&lt;/p&gt;

&lt;p&gt;A 50KB product listing response becomes ~10KB with Brotli. On mobile connections, that's a 200–400ms improvement on its own.&lt;/p&gt;

&lt;p&gt;Enable it in your nginx config:&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;brotli&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;brotli_comp_level&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;brotli_types&lt;/span&gt; &lt;span class="nc"&gt;application/json&lt;/span&gt; &lt;span class="nc"&gt;text/html&lt;/span&gt; &lt;span class="nc"&gt;text/css&lt;/span&gt; &lt;span class="nc"&gt;application/javascript&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or handle it in PHP for responses that bypass nginx caching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persisted Queries
&lt;/h2&gt;

&lt;p&gt;Instead of sending the full GraphQL query string with every request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/graphql&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="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="err"&gt;":&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter&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="n"&gt;category_id&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="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;"5\" } }) { items { id name price { regularPrice { amount { value } } } } } }"&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;Store the query on the server and send only its hash:&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;GET /graphql?queryId=a1b2c3d4&amp;amp;variables={"categoryId":"5"}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smaller request payloads (especially on mobile)&lt;/li&gt;
&lt;li&gt;GET requests are cacheable by CDN/proxy&lt;/li&gt;
&lt;li&gt;Prevents clients from executing arbitrary queries&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Field Pruning
&lt;/h2&gt;

&lt;p&gt;Magento's GraphQL resolvers often fetch more data than the client requested. Field pruning analyzes the incoming query, determines which fields are actually needed, and short-circuits database fetches for unused fields.&lt;/p&gt;

&lt;p&gt;For product detail pages, this commonly eliminates 30–50% of database queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate Limiting
&lt;/h2&gt;

&lt;p&gt;Without rate limiting, a single crawl bot or misbehaving integration can bring down your API. Implement token bucket rate limiting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free tier: 60 requests/minute per IP&lt;/li&gt;
&lt;li&gt;Authenticated tier: 600 requests/minute per API key&lt;/li&gt;
&lt;li&gt;Premium tier: unlimited&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Return &lt;code&gt;429 Too Many Requests&lt;/code&gt; with &lt;code&gt;Retry-After&lt;/code&gt; header when limits are exceeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query Complexity Analysis
&lt;/h2&gt;

&lt;p&gt;GraphQL allows deeply nested queries that can be extremely expensive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;products&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="n"&gt;items&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="n"&gt;related_products&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="n"&gt;related_products&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="n"&gt;related_products&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;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;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;Calculate a complexity score for each query and reject those above a threshold before they hit your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Optimization&lt;/th&gt;
&lt;th&gt;Implementation effort&lt;/th&gt;
&lt;th&gt;Performance gain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Response caching&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brotli compression&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persisted queries&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field pruning&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limiting&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;🛡️ Reliability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity analysis&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;🛡️ Reliability&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://magevanta.com/extensions" rel="noopener noreferrer"&gt;BetterMagento Lightweight API module&lt;/a&gt; implements all of these in a single Composer package — no custom development required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://magevanta.com/blog/magento-2-graphql-performance-guide" rel="noopener noreferrer"&gt;magevanta.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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