<?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: Dario Zadro</title>
    <description>The latest articles on DEV Community by Dario Zadro (@zadro).</description>
    <link>https://dev.to/zadro</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F361540%2F022544ef-637d-4307-8a78-0a384e700573.jpg</url>
      <title>DEV Community: Dario Zadro</title>
      <link>https://dev.to/zadro</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zadro"/>
    <language>en</language>
    <item>
      <title>Three PHP-FPM failure modes and how to actually diagnose them</title>
      <dc:creator>Dario Zadro</dc:creator>
      <pubDate>Mon, 07 Sep 2026 15:15:15 +0000</pubDate>
      <link>https://dev.to/zadro/three-php-fpm-failure-modes-and-how-to-actually-diagnose-them-2mc0</link>
      <guid>https://dev.to/zadro/three-php-fpm-failure-modes-and-how-to-actually-diagnose-them-2mc0</guid>
      <description>&lt;p&gt;Tuning guides talk about throughput. Nobody pages you about throughput. They page you about symptoms, and the useful skill is mapping a symptom back to a cause before you spend money on hardware.&lt;/p&gt;

&lt;p&gt;Three failure modes account for most of what I find on inherited servers. Each has a distinct signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 502 nobody can reproduce
&lt;/h2&gt;

&lt;p&gt;Server has 8GB. PHP-FPM is set to 100 workers. Each worker uses 60MB under load. That's 6GB of PHP, plus MariaDB, plus Nginx, plus the OS.&lt;/p&gt;

&lt;p&gt;Under normal traffic you never approach 100 workers, so it looks fine for months. Then a marketing email goes out, concurrency spikes, and the kernel runs out of memory. The OOM killer picks a process and terminates it, usually the biggest one, which is a PHP-FPM worker holding an in-flight request.&lt;/p&gt;

&lt;p&gt;User gets a 502. The application log has nothing, because the process died before it could write anything. Nginx logs &lt;code&gt;recv() failed (104: Connection reset by peer)&lt;/code&gt;. Ten minutes later everything looks normal.&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;sudo &lt;/span&gt;dmesg &lt;span class="nt"&gt;-T&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;"killed process"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-k&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; oom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hits there mean you don't have a mystery. You have a &lt;code&gt;pm.max_children&lt;/code&gt; value nobody checked against real memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The site that degrades all day and resets overnight
&lt;/h2&gt;

&lt;p&gt;TTFB is 180ms at 8am. By 4pm it's 900ms. Nobody deployed. Overnight it's fast again because something restarted PHP-FPM.&lt;/p&gt;

&lt;p&gt;That's OPcache running out of room. When the cache fills, it stops caching new scripts or wipes and rebuilds, and every miss pays full parse-and-compile again. It degrades gradually, which is why it goes unnoticed for months.&lt;/p&gt;

&lt;p&gt;The counters are &lt;code&gt;oom_restarts&lt;/code&gt; and &lt;code&gt;hash_restarts&lt;/code&gt; from &lt;code&gt;opcache_get_status()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's the part that trips people up.&lt;/strong&gt; OPcache state is per SAPI. Run that function from the CLI and you're reading the CLI cache, which is empty, separate, and tells you nothing about your site. You have to ask through PHP-FPM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// drop in webroot, lock to your IP, delete when done&lt;/span&gt;
&lt;span class="nv"&gt;$allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'203.0.113.42'&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="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REMOTE_ADDR'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$allowed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;opcache_get_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'opcache_statistics'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'hits'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$miss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'opcache_statistics'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'misses'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'hit rate:      '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$hits&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$hits&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$miss&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'cached files:  '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'opcache_statistics'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'num_cached_scripts'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
     &lt;span class="s1"&gt;' / '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'opcache_statistics'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'max_cached_keys'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'oom restarts:  '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'opcache_statistics'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'oom_restarts'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'hash restarts: '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'opcache_statistics'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'hash_restarts'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'cache full:    '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;var_export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'cache_full'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind Cloudflare or a load balancer, check what &lt;code&gt;REMOTE_ADDR&lt;/code&gt; actually contains first, or that guard is an open door. If you'd rather not touch the webroot, &lt;code&gt;cachetool&lt;/code&gt; talks to the FPM socket directly.&lt;/p&gt;

&lt;p&gt;The default &lt;code&gt;opcache.max_accelerated_files&lt;/code&gt; is 10,000. A CI4 app with modest deps is around 1,500 to 3,000 files, fine. Laravel with a normal dependency tree runs 10,000 to 15,000. Nextcloud's own health check tells admins to set it above 80,000. Count yours with &lt;code&gt;find . -name "*.php" | wc -l&lt;/code&gt; before assuming.&lt;/p&gt;

&lt;h2&gt;
  
  
  One app taking down five others
&lt;/h2&gt;

&lt;p&gt;A default install creates a single pool, &lt;code&gt;www.conf&lt;/code&gt;, shared by every site on the box. So when your least important app starts waiting on a slow third-party API, its requests occupy workers from the shared pool.&lt;/p&gt;

&lt;p&gt;Those workers aren't available to anything else. Your customer portal queues behind a lead-capture form calling an unresponsive CRM endpoint.&lt;/p&gt;

&lt;p&gt;One pool per app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; /etc/php/8.5/fpm/pool.d/crm.conf
&lt;/span&gt;&lt;span class="nn"&gt;[crm]&lt;/span&gt;
&lt;span class="py"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;www-data&lt;/span&gt;
&lt;span class="py"&gt;group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;www-data&lt;/span&gt;
&lt;span class="py"&gt;listen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/run/php/php8.5-fpm-crm.sock&lt;/span&gt;
&lt;span class="py"&gt;listen.owner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;www-data&lt;/span&gt;
&lt;span class="py"&gt;listen.group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;www-data&lt;/span&gt;

&lt;span class="py"&gt;pm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;dynamic&lt;/span&gt;
&lt;span class="py"&gt;pm.max_children&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;20&lt;/span&gt;
&lt;span class="py"&gt;pm.start_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;pm.min_spare_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;pm.max_spare_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;8&lt;/span&gt;
&lt;span class="py"&gt;pm.max_requests&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;500&lt;/span&gt;

&lt;span class="py"&gt;pm.status_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/fpm-status&lt;/span&gt;
&lt;span class="py"&gt;slowlog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/php8.5-fpm-crm-slow.log&lt;/span&gt;
&lt;span class="py"&gt;request_slowlog_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;

&lt;span class="err"&gt;php_admin_value&lt;/span&gt;&lt;span class="nn"&gt;[memory_limit]&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;256M&lt;/span&gt;
&lt;span class="err"&gt;php_admin_value&lt;/span&gt;&lt;span class="nn"&gt;[error_log]&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="err"&gt;/var/log/php8.5-fpm-crm-error.log&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Memory ceiling per app, separate logs, independent restarts. &lt;code&gt;pm.max_requests&lt;/code&gt; recycles workers so slow leaks in older code get contained instead of accumulating until the OOM killer steps in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure per-worker memory, don't guess it
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;pm.max_children&lt;/code&gt; formula everyone quotes needs a number most people invent. Measure it against warm production workers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ps &lt;span class="nt"&gt;-ylC&lt;/span&gt; php-fpm8.5 &lt;span class="nt"&gt;--sort&lt;/span&gt;:rss | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NR&amp;gt;1 {print $8/1024" MB"}'&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
58.4 MB
61.2 MB
63.9 MB
71.5 MB
88.1 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the process name with &lt;code&gt;ps -e | grep fpm&lt;/code&gt; first, since it's &lt;code&gt;php-fpm8.5&lt;/code&gt; on Debian and Ubuntu and plain &lt;code&gt;php-fpm&lt;/code&gt; elsewhere.&lt;/p&gt;

&lt;p&gt;Size for your heavy path, not your average, and leave headroom. If the math says 50, set 40. Being conservative costs you a slightly longer queue. Being aggressive costs you the OOM killer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A read-only hour
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;th&gt;Act when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OOM kills&lt;/td&gt;
&lt;td&gt;`dmesg -T \&lt;/td&gt;
&lt;td&gt;grep -i "killed process"`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU steal&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;vmstat 1 5&lt;/code&gt;, &lt;code&gt;st&lt;/code&gt; column&lt;/td&gt;
&lt;td&gt;Above 5% sustained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPcache&lt;/td&gt;
&lt;td&gt;localhost script through FPM, not CLI&lt;/td&gt;
&lt;td&gt;Hit rate under 95%, restarts non-zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File count&lt;/td&gt;
&lt;td&gt;`find . -name "*.php" \&lt;/td&gt;
&lt;td&gt;wc -l`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker memory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ps -ylC php-fpm8.5 --sort:rss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exceeds what max_children assumed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pool isolation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ls /etc/php/8.5/fpm/pool.d/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only &lt;a href="http://www.conf" rel="noopener noreferrer"&gt;www.conf&lt;/a&gt;, multiple sites&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nothing there changes anything or restarts anything. You end the hour knowing whether you have a configuration problem or a capacity problem, which is the distinction that decides whether you're making a config change or a permanent increase to your monthly bill.&lt;/p&gt;




&lt;p&gt;Longer version with the status page setup, Nginx &lt;code&gt;stub_status&lt;/code&gt;, FastCGI caching, and the cache bypass rule that burns people: &lt;a href="https://www.zadrosolutions.com/insights/nginx-php-fpm-tuning" rel="noopener noreferrer"&gt;Nginx and PHP-FPM Tuning for Mid-Market Infrastructure&lt;/a&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>nginx</category>
      <category>devops</category>
      <category>performance</category>
    </item>
    <item>
      <title>The self-hosting math for LLMs quietly inverted this year</title>
      <dc:creator>Dario Zadro</dc:creator>
      <pubDate>Mon, 07 Sep 2026 15:07:52 +0000</pubDate>
      <link>https://dev.to/zadro/the-self-hosting-math-for-llms-quietly-inverted-this-year-191f</link>
      <guid>https://dev.to/zadro/the-self-hosting-math-for-llms-quietly-inverted-this-year-191f</guid>
      <description>&lt;p&gt;Most of the self-hosting advice you'll find was written against 2024 assumptions. API pricing is expensive and linear, hardware is fixed and amortizes, so past some volume you break even. Buy the GPU, stop paying rent.&lt;/p&gt;

&lt;p&gt;Two things broke that this year, and they moved in opposite directions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inference got cheaper
&lt;/h2&gt;

&lt;p&gt;OpenAI cut GPT-5.6 pricing twice over the summer, dropping Luna by 80% and Terra by 20% at the end of July, then Sol by over 20% in August. Claude Haiku 4.5 runs $1 per million input tokens and $5 per million output, with up to 90% off cached input.&lt;/p&gt;

&lt;p&gt;Normal continuation of a multi-year trend. Nothing surprising.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardware got a lot more expensive
&lt;/h2&gt;

&lt;p&gt;The NVIDIA RTX PRO 6000 Blackwell showed up in early US retail around $8,565 in March 2025. NVIDIA's Marketplace price hit $13,250 in June 2026, then $16,000 in August. Roughly 87% above launch, same silicon, driven by the memory shortage rather than anything about the card.&lt;/p&gt;

&lt;p&gt;So if you're amortizing hardware over three years, you're betting a falling price curve stays above a fixed cost for 36 months, except the fixed cost isn't fixed either. Replacement and expansion both got more expensive while you weren't looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure your own workload first
&lt;/h2&gt;

&lt;p&gt;Here's one real query from my production telemetry. Research task on Haiku 4.5 with web search enabled:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Quantity&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input tokens&lt;/td&gt;
&lt;td&gt;28,965&lt;/td&gt;
&lt;td&gt;$0.0290&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens&lt;/td&gt;
&lt;td&gt;3,097&lt;/td&gt;
&lt;td&gt;$0.0155&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web search calls&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$0.0300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached tokens&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;$0.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;32,062 tokens, 57s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.0745&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things in there matter more than the total.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forty percent of that bill wasn't the model.&lt;/strong&gt; Three tool calls at a cent each cost more than the input tokens did. Every cost comparison you'll read argues token price, which is the half that's been falling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cached tokens were zero.&lt;/strong&gt; Most of that 29k input is a system prompt that barely changes between runs. Prompt caching would cut a large share of it. There was a cheaper optimization sitting right there, before any hardware conversation.&lt;/p&gt;

&lt;p&gt;Before you model anything, pull thirty days of usage and separate token cost from tool cost. Your request shape is probably not the one in whatever blog post you're reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The local tier is smaller than people think
&lt;/h2&gt;

&lt;p&gt;There's a persistent assumption that self-hosting means running a 70B model. Qwen's lineup doesn't have that rung. The open-weight local tier is 27B dense, with Qwen3.8-27B on Hugging Face since August under Apache 2.0. Above that it jumps to Qwen3.8-Max at 2.4T parameters as a sparse MoE.&lt;/p&gt;

&lt;p&gt;Reported memory for the 27B: roughly 56GB at BF16, about 28GB at FP8, and 14 to 17GB at 4-bit, plus KV cache. The 262k context window means budget real headroom above the weight figure.&lt;/p&gt;

&lt;p&gt;Treat those as reported, not measured. Pull the quant and watch actual resident VRAM at a realistic context length before anyone signs a purchase order.&lt;/p&gt;

&lt;p&gt;But the shape is clear: &lt;strong&gt;the local tier for narrow tasks runs on a 24GB card, not a $16,000 workstation card.&lt;/strong&gt; Those were never the same purchase. Document classification, PII redaction, entity extraction, and ticket routing are not frontier problems, and a small model fine-tuned on your domain frequently beats a general-purpose giant on exactly that kind of narrow repetitive work.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're on PHP
&lt;/h2&gt;

&lt;p&gt;Most PHP shops have historically had one option for AI work: bolt on a Python microservice and fragment the team.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/neuron-core/neuron-ai" rel="noopener noreferrer"&gt;Neuron AI&lt;/a&gt; is an agentic framework for PHP 8.1+ that keeps orchestration in the existing runtime. Multi-agent, RAG, structured outputs, built-in observability. The useful part for a mixed architecture is that swapping providers is a config change rather than a rewrite, which is the actual hedge against both lock-in and price moves. Not owning the hardware. Being able to move.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Don't self-host to save money right now. The token price is falling and the hardware price is climbing, so that trade is worse than it was eighteen months ago.&lt;/p&gt;

&lt;p&gt;Self-host what can't leave the building, route the rest to an API, and size the local side to the task rather than the frontier. The hard part isn't the infrastructure. It's classifying which workloads are which, and that has to happen before anyone writes code.&lt;/p&gt;




&lt;p&gt;Longer version with the compliance angle, the confidential computing middle path, and a decision table: &lt;a href="https://www.zadrosolutions.com/insights/self-hosted-llm-vs-api" rel="noopener noreferrer"&gt;Self-Hosted LLMs vs API Providers: Why the Math Changed&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>php</category>
      <category>architecture</category>
    </item>
    <item>
      <title>GEO Requires More Than SEO: The Technical Distinction Nobody Is Making</title>
      <dc:creator>Dario Zadro</dc:creator>
      <pubDate>Wed, 29 Apr 2026 23:48:00 +0000</pubDate>
      <link>https://dev.to/zadro/geo-requires-more-than-seo-the-technical-distinction-nobody-is-making-3hp</link>
      <guid>https://dev.to/zadro/geo-requires-more-than-seo-the-technical-distinction-nobody-is-making-3hp</guid>
      <description>&lt;p&gt;GEO is mostly SEO. But mostly is not all of it.&lt;/p&gt;

&lt;p&gt;The conversation keeps collapsing into two useless camps. Camp one says GEO is just repackaged SEO with a shinier invoice. Camp two says SEO is dead. Both are wrong.&lt;/p&gt;

&lt;p&gt;Here's the technical case for why.&lt;/p&gt;




&lt;h2&gt;
  
  
  Eligibility vs. Selection: The Distinction Nobody Is Making
&lt;/h2&gt;

&lt;p&gt;Two completely separate problems are being treated as one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eligibility&lt;/strong&gt; is whether an AI system can reach your content at all. Crawled, indexed, snippet-eligible. &lt;a href="https://developers.google.com/search/docs/appearance/ai-features" rel="noopener noreferrer"&gt;Google explicitly documents&lt;/a&gt; that to be considered for AI Overviews, a page must be indexed and eligible to show a snippet. If you don't clear that bar, nothing else matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selection&lt;/strong&gt; is what happens after retrieval fires. Of everything the system pulled in, which &lt;em&gt;chunks&lt;/em&gt; actually end up in the LLM response?&lt;/p&gt;

&lt;p&gt;SEO governs eligibility. GEO governs selection. These are sequential problems, not the same problem.&lt;/p&gt;

&lt;p&gt;The data backs this up. A &lt;a href="https://ahrefs.com/blog/ai-overview-citations-top-10/" rel="noopener noreferrer"&gt;March 2026 Ahrefs analysis&lt;/a&gt; of 863,000 keywords and 4 million AI Overview URLs found that only &lt;strong&gt;38% of cited pages ranked in the top 10&lt;/strong&gt; for the same query, down from 76% just seven months earlier. A separate &lt;a href="https://www.brightedge.com/resources/weekly-ai-search-insights/ai-overviews-one-year-presence-size-citing" rel="noopener noreferrer"&gt;BrightEdge analysis&lt;/a&gt; from February 2026 puts that overlap even lower, at around 17%.&lt;/p&gt;

&lt;p&gt;So roughly two out of three AI citations come from pages a user would never see on page one. The AI is not drawing from what ranks. It's drawing from what it can &lt;em&gt;use&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  RAG: A Term the SEO Community Is Misusing
&lt;/h2&gt;

&lt;p&gt;You can't open a LinkedIn feed right now without someone referencing RAG. Most of those posts are technically wrong.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://arxiv.org/abs/2005.11401" rel="noopener noreferrer"&gt;Retrieval-Augmented Generation&lt;/a&gt; (Lewis et al., 2020) is a specific architecture. It combines what a model already knows from training (parametric knowledge) with documents pulled in at query time (non-parametric retrieved context). That's it.&lt;/p&gt;

&lt;p&gt;RAG is &lt;strong&gt;not&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A synonym for "search API call"&lt;/li&gt;
&lt;li&gt;The same thing as embeddings, which are a vector representation method used &lt;em&gt;within&lt;/em&gt; retrieval, not the architecture itself&lt;/li&gt;
&lt;li&gt;Something that always happens. Many queries get model-only responses with no retrieval at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Retrieval is conditional, not constant. When someone tells you "rank well and you'll get retrieved," they're giving you advice that only applies to a subset of queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Not All Retrieval Runs Through Google or Bing
&lt;/h3&gt;

&lt;p&gt;Each platform retrieves differently. This is where "just do SEO" advice collapses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google AI Mode / AI Overviews.&lt;/strong&gt; Query fan-out across subtopics and data sources, with supporting pages identified &lt;em&gt;during&lt;/em&gt; response generation. The citation set extends well beyond what ranked for the original query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude.&lt;/strong&gt; Runs on Brave Search, not Google or Bing. A &lt;a href="https://www.tryprofound.com/blog/what-is-claude-web-search-explained" rel="noopener noreferrer"&gt;2025 analysis by Profound&lt;/a&gt; found 86.7% overlap between Claude citations and Brave's top organic results. Not indexed by Brave? Claude won't cite you. Doesn't matter where you rank on Google.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perplexity.&lt;/strong&gt; Runs its &lt;a href="https://docs.perplexity.ai/docs/resources/perplexity-crawlers" rel="noopener noreferrer"&gt;own continuously refreshed index&lt;/a&gt; using two distinct crawlers, after moving away from Bing Web Search API in 2022. Strong freshness bias: 90% of top cited sources answer the core question within the first 100 words (BLUF pattern).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft Copilot.&lt;/strong&gt; &lt;a href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/data-privacy-security-web-search" rel="noopener noreferrer"&gt;Grounds through Bing APIs&lt;/a&gt; across three distinct modes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI Assistants.&lt;/strong&gt; &lt;a href="https://platform.openai.com/docs/assistants/tools/file-search" rel="noopener noreferrer"&gt;Documents hybrid keyword plus semantic vector retrieval&lt;/a&gt;, query rewriting, parallel searches, and reranking. No search API involved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two distinct mechanisms underlie all of this that most agencies never separate.&lt;/p&gt;

&lt;p&gt;The first is &lt;strong&gt;training data inclusion&lt;/strong&gt;. Being present in the sources models learned from. Wikipedia, major publications, Reddit, high-authority industry sites. You build toward it through long-term brand authority and earning mentions across those sources. That's digital PR and link outreach. SEO under the hood. And Google has been parsing contextual relevance and brand mentions longer than most realize. But the real divergence is sentiment parsing across trained data and RAG methods. Feeding those systems is where SEO ends and GEO begins.&lt;/p&gt;

&lt;p&gt;The second is &lt;strong&gt;retrieval-time inclusion&lt;/strong&gt;. What happens when an LLM does live web retrieval during a prompt response. Results get re-ranked by the model based on probability and relevance. That retrieval step does not care much about rankings.&lt;/p&gt;

&lt;p&gt;That second mechanism is what 90% of GEO tactics actually target. Even when practitioners claim otherwise.&lt;/p&gt;

&lt;p&gt;Your brand appearing as a citation is an output address. It's not a retrieval receipt. It doesn't tell you how the content got there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fan-Out Coverage: The Most Underrated GEO Signal
&lt;/h2&gt;

&lt;p&gt;A single user query gets decomposed into multiple sub-queries before retrieval even happens. Your content has to survive that fan-out, showing up across several rewritten versions of the original question, not just one.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://surferseo.com/blog/query-fan-out-impact/" rel="noopener noreferrer"&gt;2025 Surfer SEO study&lt;/a&gt; analyzing 173,902 URLs found that pages ranking for fan-out queries are &lt;strong&gt;161% more likely to be cited&lt;/strong&gt; in AI Overviews than pages ranking only for the main query. The Spearman correlation between fan-out coverage and citation likelihood was 0.77. And ranking for fan-out sub-queries alone was &lt;strong&gt;49% more likely to earn a citation&lt;/strong&gt; than ranking exclusively for the head term.&lt;/p&gt;

&lt;p&gt;You can actually see fan-out in action. Open dev tools on any AI chat interface, fire a query, and inspect the Network tab. In Claude, you'll see sequential &lt;code&gt;tool_use&lt;/code&gt; blocks in the JSON response payload showing your single query decomposed into multiple sub-queries in real time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_use_blocks"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_use"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web_search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"top SEO agencies Chicago 2026"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sequence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool_use"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web_search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"generative engine optimization GEO agency Chicago"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sequence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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;One user query. Two retrieval events. Two separate source pools. Your content needs to be present across all of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Model
&lt;/h2&gt;

&lt;p&gt;Here's how most of the SEO community is currently thinking about this:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Rank. Get retrieved. Get cited.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's what's actually happening:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Be indexable. Be verifiable. Be extractable at the chunk level. Have fan-out coverage. Increase selection probability.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Those are not the same instructions.&lt;/p&gt;

&lt;p&gt;Here's a realistic split: 70% of effective GEO is SEO. 20% is new tactical work around extractability and entity signals. And 10% is genuinely novel. AI visibility monitoring, citation tracking, scrubbing fan-outs, and earning brand mentions inside answers users actually trust.&lt;/p&gt;

&lt;p&gt;GEOs who close that gap now won't need to scramble when it becomes obvious to everyone else.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full article with selection signals, platform breakdown, and measurement guidance: &lt;a href="https://zadroweb.com/blog/seo-vs-geo/" rel="noopener noreferrer"&gt;zadroweb.com/blog/seo-vs-geo/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>ai</category>
      <category>webdev</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Move Over Python: PHP Is the Sleeping Giant of AI Agents</title>
      <dc:creator>Dario Zadro</dc:creator>
      <pubDate>Wed, 29 Apr 2026 23:26:59 +0000</pubDate>
      <link>https://dev.to/zadro/move-over-python-php-is-the-sleeping-giant-of-ai-agents-577b</link>
      <guid>https://dev.to/zadro/move-over-python-php-is-the-sleeping-giant-of-ai-agents-577b</guid>
      <description>&lt;p&gt;I've been coding in PHP for over 20 years. I've watched it get declared dead at least a dozen times. And every time, it just keeps running more of the internet.&lt;/p&gt;

&lt;p&gt;So when AI started entering project needs, I didn't reach for Python. I didn't buy a seat in some workflow builder. I simply opened my existing codebase, wrote a service, and made an API call.&lt;/p&gt;

&lt;p&gt;That's it. That was the whole unlock.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Assumption Nobody Is Questioning
&lt;/h2&gt;

&lt;p&gt;Python owns AI. Everybody knows this. If you're training models or building research pipelines, Python is the right tool. Not up for debate.&lt;/p&gt;

&lt;p&gt;But here's the thing most teams aren't asking: are you actually training a model? Or are you calling an API?&lt;/p&gt;

&lt;p&gt;Because those are wildly different problems. Most businesses building "AI features" today are doing the latter. They're sending a prompt to Claude, GPT, or Gemini and doing something useful with the response. That's a REST call. REST calls are language-agnostic. And PHP has been making REST calls since before most AI startups existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  You Already Have the Stack
&lt;/h2&gt;

&lt;p&gt;PHP powers over 71% of all websites with a known server-side language. Your CRM, your CMS, your admin panel -- already PHP. And AI agents at the production layer aren't exotic research software. They're authenticated API calls, database reads, webhook handlers, queue workers.&lt;/p&gt;

&lt;p&gt;Here's something the AI industry won't tell you: most "agents" are just good old services. Strip away the marketing, and a typical AI agent is a class that accepts input, calls an external API, applies some logic, and returns output. We've been writing those since the early 2000s.&lt;/p&gt;

&lt;p&gt;The agentic wrapper adds planning, memory, and tool-calling on top. Real, and it matters. But the underlying pattern is not foreign to anyone who has spent time in an MVC framework.&lt;/p&gt;

&lt;p&gt;So why spin up a Python microservice to sit next to your PHP application, duplicate your data context across a process boundary, and introduce an entirely new runtime to maintain, just to make an HTTP request to an LLM?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Overkill Problem
&lt;/h2&gt;

&lt;p&gt;I've seen teams reach for AWS Bedrock to summarize a support ticket. I've seen n8n workflows with 14 nodes to do what is functionally a single API call. Make.com is a fantastic tool for connecting SaaS apps without code, but it is not the right answer for embedding an AI feature in a PHP application you already control.&lt;/p&gt;

&lt;p&gt;A working Claude integration in PHP:&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;$client&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;\GuzzleHttp\Client&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://api.anthropic.com/v1/messages'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'headers'&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;'x-api-key'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$_ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ANTHROPIC_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'anthropic-version'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2023-06-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'content-type'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'json'&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;'model'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'claude-haiku-4-5-20251001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'max_tokens'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'messages'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;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;'role'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$userPrompt&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="nv"&gt;$data&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getBody&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$reply&lt;/span&gt; &lt;span class="o"&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;'content'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No platform. No monthly seat. No new infrastructure. Composer, Guzzle, an API key, and you're live.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an Actual Agent Looks Like
&lt;/h2&gt;

&lt;p&gt;An API call waits for a response and returns it. An agent decides what to do next, calls tools, checks its own output, and loops until the task is done. Here's that shift in PHP using &lt;a href="https://neuron-ai.dev" rel="noopener noreferrer"&gt;Neuron AI&lt;/a&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="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Neuron&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Agent\Agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Agent\SystemPrompt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\UserMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\AIProviderInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\Anthropic\Anthropic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Tools\PropertyType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Tools\Tool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Tools\ToolProperty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FitnessAgent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;AIProviderInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="nv"&gt;$_ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ANTHROPIC_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'claude-haiku-4-5-20251001'&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;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SystemPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'You are a knowledgeable fitness assistant.'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Use available tools to look up workout plans before answering questions about them.'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Give clear, practical guidance based on the workout data returned.'&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;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;tools&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="nc"&gt;Tool&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'get_workout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Look up a workout plan by name or muscle group.'&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;addProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="s1"&gt;'workout_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="nc"&gt;PropertyType&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'The name or muscle group of the workout to retrieve.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;required&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;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setCallable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$workout_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nv"&gt;$pdo&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;\PDO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'DB_DSN'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$_ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'DB_USER'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$_ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'DB_PASS'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
                    &lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT exercises, sets, reps FROM workouts WHERE name = ?"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="nv"&gt;$stmt&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;span class="nv"&gt;$workout_name&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
                    &lt;span class="nv"&gt;$row&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;\PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FETCH_ASSOC&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;$row&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Workout not found.'&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="nv"&gt;$reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FitnessAgent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&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;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'How many sets should I do for a beginner chest workout?'&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;getMessage&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;getContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$reply&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent receives the question, decides it needs the &lt;code&gt;get_workout&lt;/code&gt; tool, calls it with the extracted muscle group or plan name, and folds the result into its response. All without you wiring that logic manually. That's the shift from API wrapper to agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  PHP's Territory
&lt;/h2&gt;

&lt;p&gt;Python wins at model training, ML research, data science pipelines, and computer vision. Uncontested. Go use Python for that.&lt;/p&gt;

&lt;p&gt;PHP's territory is the production web layer. CRMs, dashboards, ecommerce platforms, CMS systems, admin panels, business workflow tools. That layer is already PHP. The agents should be too.&lt;/p&gt;

&lt;p&gt;Your agent needs access to your authenticated user session, your database schema, your business rules, your caching layer. Transferring all of that to an external Python service isn't just overhead. It's a technical debt liability.&lt;/p&gt;

&lt;p&gt;You need an API key, a service class, and the PHP codebase you've probably already been running in production.&lt;/p&gt;

&lt;p&gt;The giant was never asleep. Everyone else just has their eyes closed.&lt;/p&gt;




&lt;p&gt;For the full breakdown including RAG in PHP, MCP, FrankenPHP, and the complete ecosystem rundown, read the full article at &lt;a href="https://zadroweb.com/blog/php-ai-agents" rel="noopener noreferrer"&gt;zadroweb.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>php</category>
      <category>laravel</category>
    </item>
    <item>
      <title>Missing Database Indexes Are Killing Your Page Speed</title>
      <dc:creator>Dario Zadro</dc:creator>
      <pubDate>Thu, 12 Mar 2026 15:54:21 +0000</pubDate>
      <link>https://dev.to/zadro/missing-database-indexes-are-killing-your-page-speed-4kod</link>
      <guid>https://dev.to/zadro/missing-database-indexes-are-killing-your-page-speed-4kod</guid>
      <description>&lt;p&gt;I provide SEO. I also build apps. And the performance problem I see most often has nothing to do with meta tags, Core Web Vitals scores, or caching plugins.&lt;/p&gt;

&lt;p&gt;It's missing database indexes.&lt;/p&gt;

&lt;p&gt;Everyone chases the green scores. Images get lazy-loaded. Fonts get preloaded. Someone installs a caching plugin and calls it done. Green across the board. Screenshot posted on LinkedIn. Everybody claps.&lt;/p&gt;

&lt;p&gt;But the uncached request still exists. The logged-in user still exists. The admin dashboard still exists. The report page pulling from six tables still exists. Caching hides the problem. It doesn't fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fast Diagnostic
&lt;/h2&gt;

&lt;p&gt;Enable MySQL's slow query log:&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;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;VARIABLES&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'slow_query_log_file'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reproduce the slow page. Check the log. Then run EXPLAIN on whatever shows up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;parent_table&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;child_table&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four columns tell the whole story: &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;key&lt;/code&gt;, &lt;code&gt;rows&lt;/code&gt;, and &lt;code&gt;Extra&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ALL&lt;/code&gt; in &lt;code&gt;type&lt;/code&gt; with &lt;code&gt;NULL&lt;/code&gt; in &lt;code&gt;key&lt;/code&gt; is the thing you don't want to see. Especially inside a subquery running once per parent row. Not mysterious. Just waste.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Foreign key columns&lt;/strong&gt; — index every column used in a JOIN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;child_table&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_parent_id&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent_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;Composite indexes&lt;/strong&gt; — column order matters. Most-filtered column goes first:&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;parent_table&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_account_status_created&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;JSON extractions&lt;/strong&gt; — can't index a computed expression directly. Use a generated column:&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;event_log&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;event_source&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;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_UNQUOTE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.source'&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="n"&gt;VIRTUAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_event_source&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_source&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;Don't overdo it&lt;/strong&gt; — every index slows writes. Index what you actually query. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  WordPress Specifically
&lt;/h2&gt;

&lt;p&gt;WordPress runs 42.5% of the web (W3Techs, March 2026). And &lt;code&gt;wp_postmeta&lt;/code&gt; does not scale gracefully under plugin pressure. Don't even get me started on RankMath's database bloat.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://wordpress.org/plugins/index-wp-mysql-for-speed/" rel="noopener noreferrer"&gt;Index WP MySQL For Speed&lt;/a&gt; plugin by Ollie Jones is a solid first move for standard installs. Heavily customized sites need a manual EXPLAIN review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Result
&lt;/h2&gt;

&lt;p&gt;A dashboard recently: 3+ seconds to load, 120,000 records, correlated subqueries hitting unindexed join columns. Thousands of row reads per load, every single request.&lt;/p&gt;

&lt;p&gt;Three index statements. Instant. Same server, same code, same database.&lt;/p&gt;




&lt;p&gt;Check the DB structure before you blame the host or code. The answer is usually closer than you think.&lt;/p&gt;

&lt;p&gt;Full breakdown on the Zadro Web blog: &lt;a href="https://zadroweb.com/blog/database-indexes-slow-website/" rel="noopener noreferrer"&gt;https://zadroweb.com/blog/database-indexes-slow-website/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mysql</category>
      <category>webdev</category>
      <category>performance</category>
      <category>wordpress</category>
    </item>
  </channel>
</rss>
