<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>WP-Cron | John Nessime</title>
	<atom:link href="https://john-nessime.com/blog/tag/wp-cron/feed/" rel="self" type="application/rss+xml" />
	<link>https://john-nessime.com/blog/tag/wp-cron/</link>
	<description>Cloud, DevOps, Data &#38; AI — Built, Tested, Explained</description>
	<lastBuildDate>Tue, 04 Aug 2026 12:05:50 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.3</generator>

<image>
	<url>https://john-nessime.com/blog/wp-content/uploads/2026/07/cropped-jn-32x32.png</url>
	<title>WP-Cron | John Nessime</title>
	<link>https://john-nessime.com/blog/tag/wp-cron/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>WP-CLI Commands That Save Hours (And the One That Bites Back)</title>
		<link>https://john-nessime.com/blog/technical-guides/wp-cli-commands-that-save-hours/</link>
					<comments>https://john-nessime.com/blog/technical-guides/wp-cli-commands-that-save-hours/#respond</comments>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Sun, 09 Aug 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[System Administration]]></category>
		<category><![CDATA[Technical Guides]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Automation]]></category>
		<category><![CDATA[Bash]]></category>
		<category><![CDATA[Command Line]]></category>
		<category><![CDATA[search-replace]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[Sysadmin]]></category>
		<category><![CDATA[Troubleshooting]]></category>
		<category><![CDATA[WordPress Database]]></category>
		<category><![CDATA[WordPress Maintenance]]></category>
		<category><![CDATA[WordPress Migration]]></category>
		<category><![CDATA[WP-CLI]]></category>
		<category><![CDATA[WP-Cron]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=171</guid>

					<description><![CDATA[<p>The dashboard is fine for editing a post. It is a terrible tool for anything touching a thousand rows, twelve plugins or two servers. Here are the WP-CLI commands that save hours on migrations, plugin conflicts, stalled cron and integrity checks, grouped by the job that eats the time, with the failure modes each one hides.</p>
<p>The post <a href="https://john-nessime.com/blog/technical-guides/wp-cli-commands-that-save-hours/">WP-CLI Commands That Save Hours (And the One That Bites Back)</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The ticket said &#8220;staging is approved, push it live.&#8221; Copy the files, import the database, change the two URL fields in Settings, done. The homepage loads. Then the slider is empty, a third of the images 404, and the page builder renders a grey box where a section used to be.</p>



<p class="wp-block-paragraph">That is the moment most people properly meet the command line, usually at the worst possible time. The dashboard is fine for editing a post. It is a bad tool for anything that touches a thousand rows, twelve plugins, or two servers at once.</p>



<p class="wp-block-paragraph">This is a working list of the WP-CLI commands that save hours, grouped by the job that eats the hours rather than by command name. Each section leads with the failure mode, because with a couple of these the command finishes, prints <code>Success</code>, and leaves you with a subtly broken database that nobody notices for a week.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The migration hour: search-replace, and the one that bites back</h2>



<p class="wp-block-paragraph">Changing a domain by running SQL against <code>wp_posts</code> is how sites break. WordPress stores a great deal of configuration as PHP serialized arrays, and serialized strings carry their own byte length. Change <code>https://old.example.com</code> to <code>https://new.example.org</code> with a plain <code>UPDATE ... REPLACE()</code> and the length prefix no longer matches the string. PHP refuses to unserialize the value, returns <code>false</code>, and the widget or page-builder section silently renders nothing.</p>



<p class="wp-block-paragraph">The whole point of <code>wp search-replace</code> is that it unserializes, replaces, and reserializes, so the lengths stay correct. Run it in dry-run mode first, every single time:</p>



<pre class="wp-block-code"><code>wp search-replace 'https://old.example.com' 'https://new.example.org' 
  --all-tables-with-prefix 
  --skip-columns=guid 
  --precise 
  --report-changed-only 
  --dry-run</code></pre>



<p class="wp-block-paragraph">What each flag is actually doing:</p>



<ul class="wp-block-list">
<li><code>--dry-run</code> runs the whole operation and prints the report without writing. The report has a &#8220;Type&#8221; column: <code>PHP</code> means the value was serialized and handled properly, <code>SQL</code> means a plain string replacement. If you expected hundreds of changes and see zero, your search string is wrong, not the tool.</li>

<li><code>--all-tables-with-prefix</code> widens the scope to every table sharing your prefix, not just the tables registered with <code>$wpdb</code>. Plugins that create their own tables live here. <code>--all-tables</code> goes wider still and will happily rewrite tables belonging to a completely different application sharing the database, so reach for the prefix version first.</li>

<li><code>--skip-columns=guid</code> is the one people leave off. The <code>guid</code> column is a permanent identifier for feed readers, not a URL to be followed. Rewriting it makes every historical post look brand new to anything that has subscribed to your feed.</li>

<li><code>--precise</code> forces PHP-side serialization handling on every column instead of letting MySQL do the simple cases. It is slower. On a large database it is noticeably slower. It is also the difference between &#8220;probably fine&#8221; and &#8220;definitely correct&#8221;.</li>

<li><code>--report-changed-only</code> trims the report to tables that actually changed, which makes the output readable instead of a wall of zeros.</li>
</ul>



<p class="wp-block-paragraph">Read the dry-run output. When the numbers look right, run the identical command without <code>--dry-run</code>.</p>



<h3 class="wp-block-heading">The step everyone forgets</h3>



<p class="wp-block-paragraph">You changed the database. Every cache layer still holds the old values. On a site with Redis or Memcached behind a persistent object cache drop-in, the old URLs will keep being served from memory and you will spend twenty minutes convinced the replacement failed.</p>



<pre class="wp-block-code"><code>wp cache flush
wp transient delete --expired
wp rewrite flush</code></pre>



<p class="wp-block-paragraph">Be aware that on multisite with a shared persistent object cache, <code>wp cache flush</code> can clear entries for every site on the network, not just the one you are working on. Then purge the page cache and the CDN through their own controls. If you are on Cloudflare, the database change means nothing until the edge cache is purged too.</p>



<p class="wp-block-paragraph">Finally, re-run the dry-run. It should report zero remaining replacements. If it does not, you have a second URL format hiding somewhere: protocol-relative <code>//old.example.com</code>, or an escaped form like <code>https://old.example.com</code> inside JSON stored in an option. Run a second pass for each shape you find.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The plugin conflict hour: bisect without touching the dashboard</h2>



<p class="wp-block-paragraph">Classic scenario: something fatals, the admin is white, and the usual advice is &#8220;deactivate all plugins and switch to a default theme.&#8221; On a production site that means real downtime while you click through a list.</p>



<p class="wp-block-paragraph">WP-CLI has two global parameters that skip loading plugins and themes for the duration of a single command. Nothing is deactivated. Nothing is written. The site keeps serving visitors exactly as it was.</p>



<pre class="wp-block-code"><code># Does WP-CLI work at all with nothing loaded?
wp --skip-plugins --skip-themes option get siteurl

# Is it the theme?
wp --skip-themes option get siteurl

# Skip one specific plugin
wp --skip-plugins=akismet option get siteurl</code></pre>



<p class="wp-block-paragraph">If the first command works and the unmodified one does not, the fault is in your plugins or theme, not in WP-CLI or WordPress core. From there you can walk the active list one at a time. This one-liner from the WP-CLI handbook does the walk for you:</p>



<pre class="wp-block-code"><code>wp plugin list --field=name --status=active --skip-plugins 
  | xargs -n1 -I % wp --skip-plugins=% plugin get % --field=name</code></pre>



<p class="wp-block-paragraph">The inner <code>--skip-plugins</code> on the listing command matters. Without it, a plugin broken badly enough to crash the bootstrap will also crash the command that is trying to build the list, and you get nothing.</p>



<p class="wp-block-paragraph">The honest limitation: skipping a plugin at the CLI does not reproduce browser-side behaviour. If the bug only appears for logged-in users on a specific admin screen, this narrows the field but will not fully confirm the culprit. It still turns a thirty-minute click-through into about two minutes of scrolling output.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The silent-cron hour: scheduled jobs that quietly stopped</h2>



<p class="wp-block-paragraph">This is the invisible failure. Nobody opens a ticket saying &#8220;cron is broken.&#8221; They open a ticket saying backups have not run since last month, or scheduled posts are stuck in the future, or order emails stopped going out. WP-Cron is not a scheduler. It is a queue that gets drained when someone loads a page, which means a low-traffic site drains it rarely and a fully page-cached site may never drain it at all.</p>



<pre class="wp-block-code"><code># Can the site reach its own wp-cron.php?
wp cron test

# What is scheduled, and how overdue is it?
wp cron event list

# Drain everything that is due, right now, in this process
wp cron event run --due-now</code></pre>



<p class="wp-block-paragraph">In <code>wp cron event list</code>, the column to read is <code>next_run_relative</code>. If events are showing as long overdue, the queue is not being drained and you have found your problem.</p>



<p class="wp-block-paragraph">The fix is to stop relying on page loads. Disable the loopback trigger in <code>wp-config.php</code>, above the &#8220;stop editing&#8221; line:</p>



<pre class="wp-block-code"><code>define( 'DISABLE_WP_CRON', true );</code></pre>



<p class="wp-block-paragraph">Then add a real system cron entry for the user that owns the site files:</p>



<pre class="wp-block-code"><code>* * * * * /usr/local/bin/wp --path=/var/www/example.com cron event run --due-now --quiet</code></pre>



<p class="wp-block-paragraph">Why the CLI form and not a <code>curl</code> of <code>wp-cron.php</code>: <code>wp cron event run</code> executes the events in the current PHP process. No HTTP loopback is involved, so it cannot be defeated by a firewall rule, a reverse proxy, HTTP authentication on staging, or a host that rate-limits self-requests. Errors surface in the cron mail or the log you redirect to, instead of vanishing into a request nobody reads.</p>



<p class="wp-block-paragraph">One warning. Set <code>DISABLE_WP_CRON</code> and then forget the crontab entry, and you have upgraded an unreliable scheduler into one that never runs at all. Verify with <code>wp cron event list</code> a few minutes later before you close the ticket.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The bulk-edit hour: piping IDs instead of clicking pages</h2>



<p class="wp-block-paragraph">The pattern that does the real work here is <code>--format=ids</code> feeding another command. Once that clicks, most &#8220;there is no plugin for this&#8221; tasks become one line.</p>



<pre class="wp-block-code"><code># How many revisions are we carrying?
wp post list --post_type=revision --format=count

# Delete them, skipping the trash
wp post delete $(wp post list --post_type=revision --format=ids) --force

# Every administrator on the site, in one table
wp user list --role=administrator --fields=ID,user_login,user_email

# Regenerate only the thumbnail sizes that are missing
wp media regenerate --only-missing --yes</code></pre>



<p class="wp-block-paragraph">Two things to know before you run the delete. It is irreversible, so take a database export first. And if the inner command returns nothing because there are no revisions, the outer command gets no arguments and will error rather than doing nothing quietly. That is a harmless failure, but it looks alarming the first time.</p>



<p class="wp-block-paragraph">Revisions come back unless you cap them. Adding <code>define( 'WP_POST_REVISIONS', 5 );</code> to <code>wp-config.php</code> keeps enough history to recover from a bad edit without letting <code>wp_posts</code> grow without limit.</p>



<p class="wp-block-paragraph">For anything you want to script rather than read, add <code>--format=json</code> and pipe it into <code>jq</code>. Every list command in WP-CLI supports it, which makes the whole tool usable as a data source for monitoring and reporting rather than just an admin replacement.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The &#8220;is this thing compromised?&#8221; hour: checksums</h2>



<p class="wp-block-paragraph">A site is behaving oddly, there is a suspicious redirect, and the first instinct is to install a security scanner. Before that, run the two commands that compare what is on disk against the official hashes on WordPress.org.</p>



<pre class="wp-block-code"><code>wp core verify-checksums
wp plugin verify-checksums --all</code></pre>



<p class="wp-block-paragraph">These download the published checksums for your exact version and locale and report every core or plugin file that has been modified, plus files that should not exist at all. The core command deliberately avoids loading WordPress, so it still works when the site itself is too broken to boot.</p>



<p class="wp-block-paragraph">Know the blind spots before you trust a clean result:</p>



<ul class="wp-block-list">
<li>Only plugins hosted on the WordPress.org directory can be verified. Premium plugins, custom code and anything from a private repository have no published source of truth and will be skipped or warned about.</li>

<li>Nothing in <code>wp-content/uploads</code> is covered. A dropped PHP file in an uploads subdirectory is a common backdoor and checksums will never see it.</li>

<li>Themes are not covered by an equivalent bundled command.</li>

<li>Getting a locale or version mismatch produces alarming warnings that are not real findings. Pass <code>--version</code> and <code>--locale</code> explicitly if the output looks wrong.</li>
</ul>



<p class="wp-block-paragraph">A clean checksum run is not proof the site is clean. A dirty one is proof it is not, which is worth a great deal in the first five minutes of an incident.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The database hour: find the bloat before you optimise anything</h2>



<p class="wp-block-paragraph">&#8220;The site is slow&#8221; is not actionable. &#8220;One table is carrying most of the database&#8221; is. Start by looking, not by running an optimiser.</p>



<pre class="wp-block-code"><code># Size per table, sorted by whatever is worst
wp db size --tables --human-readable

# Take an export before touching anything
wp db export ~/backups/example-com.sql

# Run a query and strip the ASCII table borders
wp db query 'SELECT option_value FROM wp_options WHERE option_name="home"' --skip-column-names</code></pre>



<p class="wp-block-paragraph">In practice the offenders are nearly always <code>wp_options</code> stuffed with autoloaded rows and orphaned transients, <code>wp_postmeta</code> from a plugin that never cleans up after itself, or <code>wp_posts</code> full of revisions. Autoloaded options are the sneaky one, because every row marked for autoload is loaded on every request, including requests that will never look at it.</p>



<p class="wp-block-paragraph">Two clean-up commands worth knowing, and the difference between them:</p>



<pre class="wp-block-code"><code>wp transient delete --expired
wp transient delete --all</code></pre>



<p class="wp-block-paragraph">The first removes only what has already expired and is safe to run on a schedule. The second removes everything, including live cache entries, which forces WordPress to rebuild them on the next requests. That is safe but produces a short performance dip, so it belongs in a maintenance window rather than in a cron job.</p>



<p class="wp-block-paragraph">Note that on a site with a persistent object cache drop-in installed, transients bypass the database entirely and wrap the object cache instead. If <code>wp transient delete --all</code> reports nothing, that is usually the reason and not a failure.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The setup that makes everything above faster</h2>



<p class="wp-block-paragraph">If you manage more than one site, the biggest single time saving is not a command at all. It is aliases. Put a <code>wp-cli.yml</code> in a project directory, or <code>~/.wp-cli/config.yml</code> for a global one:</p>



<pre class="wp-block-code"><code>@production:
  ssh: deploy@example.com/var/www/example.com
@staging:
  ssh: deploy@staging.example.com/var/www/staging</code></pre>



<p class="wp-block-paragraph">Now you run commands against a remote site from your own machine, without an SSH session:</p>



<pre class="wp-block-code"><code>wp @staging plugin list --status=active
wp @production cron event list</code></pre>



<p class="wp-block-paragraph">This needs WP-CLI installed on the remote host, and it needs your host to actually give you SSH. That varies more than people expect. A plain VPS from a provider like InterServer or DigitalOcean gives you root and you install what you like. Managed WordPress hosts differ: some ship WP-CLI preinstalled and hand you an SSH key immediately, others restrict which commands you can run, and a few give you no shell at all on entry-level plans. Worth checking before you build a workflow that depends on it.</p>



<p class="wp-block-paragraph">Two optional packages are worth the install if you do maintenance work regularly:</p>



<pre class="wp-block-code"><code>wp package install wp-cli/doctor-command
wp package install wp-cli/profile-command

wp doctor check --all
wp profile stage</code></pre>



<p class="wp-block-paragraph"><code>wp doctor</code> runs a set of health checks, including one for autoloaded options size, and gives you a pass or warn per check. <code>wp profile</code> breaks a request into stages so you can see where the time is going before you start guessing.</p>



<p class="wp-block-paragraph">The honest trade-off: these are separate packages, not bundled commands, and they get less attention than the core set. If you are working across client servers you do not control, installing packages everywhere is friction you may not want. On your own infrastructure they earn their place. For continuous visibility rather than point-in-time checks you still want proper monitoring, whether that is a self-hosted Prometheus and Grafana stack or a hosted uptime service.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Troubleshooting: when WP-CLI itself will not run</h2>



<p class="wp-block-paragraph">Before blaming the tool, run <code>wp cli info</code>. It prints the PHP binary in use, the PHP version, the loaded <code>php.ini</code>, and the WP-CLI version. Most problems are visible in that output.</p>



<ul class="wp-block-list">
<li><strong>&#8220;This does not seem to be a WordPress installation.&#8221;</strong> You are in the wrong directory, or the document root is elsewhere. Pass <code>--path=/full/path/to/wordpress</code> rather than guessing.</li>

<li><strong>Wrong PHP version.</strong> On a control panel server the shell PHP is often not the PHP the site runs on. Check the binary in <code>wp cli info</code>, and invoke the right one explicitly if they differ, for example <code>/usr/local/php83/bin/php $(which wp)</code>.</li>

<li><strong>Memory exhaustion on large operations.</strong> The CLI uses its own memory limit, not the web one. Raise it for a single command with <code>php -d memory_limit=512M $(which wp) ...</code> instead of editing configuration globally.</li>

<li><strong>A fatal error the moment you type anything.</strong> Confirm with <code>wp --skip-plugins --skip-themes cli info</code>. If that works, the fault is in site code.</li>

<li><strong>A warning about running as root.</strong> WP-CLI is telling you that files created by this command will be owned by root and the web server may not be able to write to them later. <code>--allow-root</code> silences the warning without fixing the ownership problem. Run as the site user instead, unless you are inside a container where root is the normal case.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Common mistakes</h2>



<ul class="wp-block-list">
<li>Running <code>search-replace</code> without <code>--dry-run</code> because you have done it before and it worked.</li>

<li>Reaching for <code>--all-tables</code> by reflex when <code>--all-tables-with-prefix</code> is what you meant, on a server where several sites share one database.</li>

<li>Forgetting to flush the object cache and CDN after a database write, then re-running the replacement several times chasing a ghost.</li>

<li>Setting <code>DISABLE_WP_CRON</code> without adding the system cron entry.</li>

<li>Treating a clean <code>verify-checksums</code> result as proof that a site is not compromised.</li>

<li>Running destructive commands over SSH without <code>screen</code> or <code>tmux</code>, then losing the connection mid-write on a large table.</li>

<li>Using <code>--allow-root</code> as a habit and leaving root-owned files scattered through <code>wp-content</code>.</li>
</ul>



<h2 class="wp-block-heading">Best practices</h2>



<ol class="wp-block-list">
<li>Export the database before any command that writes. <code>wp db export</code> takes seconds and has saved more afternoons than any other command in this post.</li>

<li>Dry-run first, read the report, then repeat the command verbatim without the flag. Do not retype it.</li>

<li>Run as the site user, not root, so file ownership stays correct.</li>

<li>Use aliases in <code>wp-cli.yml</code> so you cannot run a staging command against production by muscle memory.</li>

<li>Verify after the write. Re-run the read-only version of whatever you just did and confirm it reports zero remaining work.</li>

<li>Test the restore path, not just the backup. An export you have never imported is a hope, not a backup.</li>

<li>Keep WP-CLI current with <code>wp cli update</code>, particularly before working on a site running a recent PHP release.</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Frequently asked questions</h2>



<h3 class="wp-block-heading">Is wp search-replace safe to run on a live site?</h3>



<p class="wp-block-paragraph">Safer than raw SQL, because it handles serialized data correctly. Still a write against production. Export the database first, dry-run first, and use <code>--skip-columns=guid</code>. On a large database the operation can hold the site in a slightly inconsistent state for the duration, so a quiet window helps.</p>



<h3 class="wp-block-heading">Why do my changes not show up after search-replace?</h3>



<p class="wp-block-paragraph">Almost always caching. Run <code>wp cache flush</code>, purge the page cache and the CDN, then reload. If the old value still appears in a dry-run report, you have a second URL format stored somewhere, such as a protocol-relative or escaped variant.</p>



<h3 class="wp-block-heading">Does WP-CLI need SSH access?</h3>



<p class="wp-block-paragraph">It needs shell access on a machine that can reach the WordPress files and database. That usually means SSH. Some managed hosts provide a browser terminal instead, and some restrict the available commands. On a self-managed VPS you install it yourself and nothing is restricted.</p>



<h3 class="wp-block-heading">What is the difference between &#8211;skip-plugins and deactivating a plugin?</h3>



<p class="wp-block-paragraph"><code>--skip-plugins</code> prevents the plugin from loading for that one command only. Nothing is written to the database and visitors are unaffected. Deactivating changes site state and takes the plugin offline for everyone, which is why the skip flags are the right diagnostic tool on production.</p>



<h3 class="wp-block-heading">Can I schedule WP-CLI commands with cron?</h3>



<p class="wp-block-paragraph">Yes, and that is the recommended way to run WordPress scheduled events reliably. Use the full path to the <code>wp</code> binary, pass <code>--path</code>, run as the site user, and redirect output so failures are visible rather than silent.</p>



<h3 class="wp-block-heading">Will verify-checksums detect all malware?</h3>



<p class="wp-block-paragraph">No. It compares core files and WordPress.org-hosted plugins against published hashes. It does not cover themes, premium or custom plugins, or anything in the uploads directory. Treat a failure as a definite finding and a pass as one data point among several.</p>



<h3 class="wp-block-heading">Which WP-CLI commands should I learn first?</h3>



<p class="wp-block-paragraph"><code>wp db export</code>, <code>wp search-replace --dry-run</code>, <code>wp --skip-plugins</code>, and <code>wp cron event list</code>. Those four cover backup, migration, conflict isolation and the most common silent failure on a WordPress site.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The one thing worth remembering</h2>



<p class="wp-block-paragraph">Most of the WP-CLI commands that save hours do it by turning a job you cannot see into a job you can read. <code>--dry-run</code> shows you the write before it happens. <code>wp cron event list</code> shows you a queue that was failing silently. <code>verify-checksums</code> shows you a file that changed without anyone deciding it should.</p>



<p class="wp-block-paragraph">The speed is real, but the speed is not the point. The point is that you stop guessing. If you take one habit from this post, take the dry-run: run the read-only version, read the output, then run the identical command for real. That single pattern prevents the majority of the incidents I see caused by command-line work on WordPress.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Need a hand with this?</h2>



<p class="wp-block-paragraph">I work on WordPress infrastructure, and command-line automation is most of what makes that maintainable. Things I can help with directly:</p>



<ul class="wp-block-list">
<li>Domain and host migrations run properly, with staged dry-runs, correct table scoping and a verified rollback before anything is written.</li>

<li>Replacing WP-Cron with real system cron across a fleet, including alerting when a job stops firing instead of finding out weeks later.</li>

<li>Writing repeatable WP-CLI maintenance scripts, with proper exit codes and logging, so they can be scheduled rather than remembered.</li>

<li>Diagnosing slow WordPress sites at the database layer, from autoloaded options and table bloat down to the query causing it.</li>

<li>Incident triage on a site suspected of being compromised, starting with integrity checks and file-level evidence rather than a scanner plugin.</li>

<li>Setting up multi-site management workflows with WP-CLI aliases, SSH keys and deployment pipelines that do not depend on anyone opening the dashboard.</li>
</ul>



<p class="wp-block-paragraph">If something specific is going wrong, send the actual output. A dry-run report, a <code>wp cli info</code> dump, a <code>wp cron event list</code> table, and I can usually tell you where the problem is before we talk about scope.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Work with me on Upwork</a></div>
</div>
<p>The post <a href="https://john-nessime.com/blog/technical-guides/wp-cli-commands-that-save-hours/">WP-CLI Commands That Save Hours (And the One That Bites Back)</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://john-nessime.com/blog/technical-guides/wp-cli-commands-that-save-hours/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Your Site Is Fast and Your Dashboard Is Not: Fixing a Slow WordPress Admin</title>
		<link>https://john-nessime.com/blog/wordpress/slow-wordpress-admin/</link>
					<comments>https://john-nessime.com/blog/wordpress/slow-wordpress-admin/#respond</comments>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Sat, 01 Aug 2026 09:24:34 +0000</pubDate>
				<category><![CDATA[Troubleshooting]]></category>
		<category><![CDATA[Web Performance]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Autoloaded Options]]></category>
		<category><![CDATA[Caching]]></category>
		<category><![CDATA[Database Optimization]]></category>
		<category><![CDATA[Heartbeat API]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Object Cache]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Query Monitor]]></category>
		<category><![CDATA[Website Performance]]></category>
		<category><![CDATA[WordPress Debugging]]></category>
		<category><![CDATA[WordPress Hosting]]></category>
		<category><![CDATA[wp-admin]]></category>
		<category><![CDATA[WP-CLI]]></category>
		<category><![CDATA[WP-Cron]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=58</guid>

					<description><![CDATA[<p>Your homepage loads instantly and your dashboard takes eight seconds. That is not a contradiction: wp-admin is the only part of your site that never gets cached. Here is where the time actually goes, in the order worth checking.</p>
<p>The post <a href="https://john-nessime.com/blog/wordpress/slow-wordpress-admin/">Your Site Is Fast and Your Dashboard Is Not: Fixing a Slow WordPress Admin</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The ticket says the site is fine but the dashboard takes eight seconds. You check the homepage: loads instantly, green across the board in every speed test you throw at it. You log in, click Posts, and watch the spinner. Same server, same database, same plugins. One is fast and one is not.</p>



<p class="wp-block-paragraph">Here is the thing that explains almost every case of this, and it is worth getting straight before you touch a single setting: <strong>wp-admin is the only part of your site that never gets cached.</strong> Every page cache plugin, every CDN, every reverse proxy in front of WordPress explicitly excludes logged-in requests. It has to, otherwise you would serve one editor&#8217;s draft screen to another. So the front end you keep measuring is a static HTML file being handed over by Cloudflare or Nginx, and the dashboard is your actual application running end to end, every single time.</p>



<p class="wp-block-paragraph">A <strong>slow WordPress admin</strong> is not a separate problem from a slow site. It is the same problem, visible. The cache was hiding it, not fixing it.</p>



<p class="wp-block-paragraph">This post walks through where that time actually goes, in the order I check things: how to tell server time from browser time, the <code>wp_options</code> table, outbound HTTP requests that block your page load, admin-ajax and the heartbeat, wp-cron, database bloat, and the server underneath it. Plus the troubleshooting steps for the cases that do not fit the pattern.</p>



<h2 class="wp-block-heading">First, split the problem in two</h2>



<p class="wp-block-paragraph">Do not start changing things. Find out whether you are waiting on the server or on the browser, because the fixes have nothing in common.</p>



<p class="wp-block-paragraph">Open the browser dev tools, go to the Network tab, hard reload an admin page, and look at the document request. If TTFB on that first request is three seconds, the server is thinking and nothing you do to JavaScript will help. If TTFB is 200ms and the page still feels slow, you have a rendering problem: too many scripts, a bloated admin notice area, a media grid loading hundreds of full-size images.</p>



<p class="wp-block-paragraph">For server-side time, install <a href="https://wordpress.org/plugins/query-monitor/" target="_blank" rel="noreferrer noopener">Query Monitor</a>. It is the single most useful diagnostic tool in the WordPress ecosystem and it costs nothing. It gives you, per admin page load: total queries and their time, which queries came from which plugin, every HTTP request WordPress made server-side and how long each took, hook timings, and PHP errors. Nine times out of ten the answer is sitting in the HTTP API panel or the Queries panel, and you will have it in under a minute.</p>



<p class="wp-block-paragraph">One caution: Query Monitor itself adds overhead, so use it to find the culprit, then deactivate it before you measure the improvement.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The invisible one: your autoloaded options</h2>



<p class="wp-block-paragraph">This is the failure that hides for years and then bites everybody at once, and it is where I look first on any slow WordPress admin.</p>



<p class="wp-block-paragraph">WordPress loads every option marked as autoload in one query at the start of <em>every single request</em>, then unserializes the lot into memory. That design is fine when the total is a few dozen kilobytes. It stops being fine when a page builder decides to autoload its full asset manifest, a slider plugin stores every slide&#8217;s settings in one row, an analytics plugin caches an API response there, and three plugins you uninstalled two years ago left their rows behind.</p>



<p class="wp-block-paragraph">Now every admin click pulls several megabytes out of MySQL and runs it through PHP&#8217;s unserializer before WordPress has drawn anything. No error, no warning, no slow query in the log worth noticing. Just a tax on every request.</p>



<h3 class="wp-block-heading">Measuring it</h3>



<p class="wp-block-paragraph">There is a trap in the usual SQL query you will find online. WordPress 6.6 changed the <code>autoload</code> column from a simple yes/no to a set of values: <code>on</code>, <code>off</code>, <code>auto</code>, <code>auto-on</code>, <code>auto-off</code>. Older rows keep <code>yes</code> and <code>no</code>, and there is no upgrade routine, so a real site has a mix. Querying only for <code>autoload = 'yes'</code> undercounts, sometimes badly.</p>



<pre class="wp-block-code"><code>-- Total bytes loaded on every request.
-- Adjust wp_ if your table prefix is different.
SELECT SUM(LENGTH(option_value)) AS autoload_bytes
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto', 'auto-on');

-- The worst offenders, largest first.
SELECT option_name, LENGTH(option_value) AS size_bytes
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto', 'auto-on')
ORDER BY size_bytes DESC
LIMIT 25;</code></pre>



<p class="wp-block-paragraph">If you have WP-CLI, this is quicker and you do not need database credentials:</p>



<pre class="wp-block-code"><code># Total size of everything autoloaded, in bytes.
wp option list --autoload=on --format=total_bytes

# Biggest autoloaded options. Note the sort happens in the shell:
# wp option list only sorts by option_id, option_name or option_value.
wp option list --autoload=on --fields=option_name,size_bytes | sort -n -k 2 | tail -25</code></pre>



<p class="wp-block-paragraph">Site Health flags this too, under Tools then Site Health, once the total crosses core&#8217;s threshold. Treat that warning as an audit trigger rather than a number to chase.</p>



<h3 class="wp-block-heading">Fixing it without breaking anything</h3>



<p class="wp-block-paragraph">Read the list before you delete anything. You are looking for three categories:</p>



<ul class="wp-block-list">
<li><strong>Orphans.</strong> Options with the prefix of a plugin that is no longer installed. Safe to delete after a backup.</li>
<li><strong>Wrongly autoloaded.</strong> Settings for a plugin that only ever runs on one admin screen. These should not be autoloaded, but they should not be deleted either.</li>
<li><strong>Cache data in the wrong place.</strong> A plugin storing an API response or an asset map as a permanent option. Usually safe to delete, because the plugin will rebuild it, but check that assumption on staging first.</li>
</ul>



<p class="wp-block-paragraph">For the middle category, flip autoload off instead of deleting. Use the WordPress API rather than raw SQL, because a direct <code>UPDATE</code> leaves the object cache holding the old value and you will spend twenty minutes convinced nothing changed:</p>



<pre class="wp-block-code"><code># Stop autoloading a specific option. Run once, then remove.
wp eval "wp_set_option_autoload( 'some_plugin_bulky_setting', false );"</code></pre>



<p class="wp-block-paragraph">While you are in there, clear out expired transients. They live in the same table and orphaned ones accumulate quietly, especially on sites that have been through a few plugin migrations:</p>



<pre class="wp-block-code"><code>wp transient delete --expired</code></pre>



<h2 class="wp-block-heading">Outbound HTTP requests that block your page load</h2>



<p class="wp-block-paragraph">This one is regional and it catches people out badly. WordPress makes server-side HTTP calls during admin requests: core update checks, plugin and theme update checks, the Events and News dashboard widget, and whatever your commercial plugins do to phone home for licence validation.</p>



<p class="wp-block-paragraph">Those calls are synchronous. If the endpoint is slow to reach from your server, or a firewall is silently dropping the packets rather than refusing them, PHP sits there until the timeout expires. Two plugins doing that with a five-second timeout each is ten seconds of dashboard, and absolutely nothing in the interface tells you why.</p>



<p class="wp-block-paragraph">Query Monitor&#8217;s HTTP API panel shows every one of these with its duration, which is usually enough. To confirm from the server directly:</p>



<pre class="wp-block-code"><code># How long does the server take to reach the WordPress.org API?
# -w prints the timing, -o discards the body, -s hides the progress meter.
curl -w "%{time_total}n" -o /dev/null -s https://api.wordpress.org/core/version-check/1.7/</code></pre>



<p class="wp-block-paragraph">If that takes seconds, or hangs, you have found it. For a pure diagnostic, you can temporarily block all outbound HTTP from WordPress and see whether the dashboard snaps back:</p>



<pre class="wp-block-code"><code>// wp-config.php, ABOVE the "stop editing" line.
// DIAGNOSTIC ONLY. This breaks updates, licence checks and
// anything else that calls out. Remove it once you have your answer.
define( 'WP_HTTP_BLOCK_EXTERNAL', true );

// Optionally allow specific hosts back through while testing.
define( 'WP_ACCESSIBLE_HOSTS', 'api.wordpress.org,*.wordpress.org' );</code></pre>



<p class="wp-block-paragraph">Do not leave that in place as a fix. If outbound connectivity is genuinely the problem, the real solution is at the network layer: fix the firewall rule, fix DNS resolution on the box, or move to a host that does not throttle outbound connections. If you are on shared hosting and cannot influence any of that, a small VPS from somewhere like InterServer will usually behave better than a crowded shared box, purely because you control the network stack.</p>



<p class="wp-block-paragraph">The dashboard news widget is the one piece here you can just remove, and most teams never look at it:</p>



<pre class="wp-block-code"><code>// In a site-specific plugin, not in a parent theme.
add_action( 'wp_dashboard_setup', function () {
    remove_meta_box( 'dashboard_primary', 'dashboard', 'side' );
} );</code></pre>



<h2 class="wp-block-heading">admin-ajax.php and the heartbeat</h2>



<p class="wp-block-paragraph">Open the Network tab in dev tools, filter on <code>admin-ajax.php</code>, and leave a dashboard tab sitting idle for two minutes. You will see requests firing on a timer. That is the Heartbeat API, and it powers autosave, post locking, and session expiry warnings. Those are real features and you should not rip them out.</p>



<p class="wp-block-paragraph">The cost is that each tick boots WordPress, loads every active plugin, runs any handlers hooked into it, and occupies a PHP worker for the duration. The post editor ticks fastest because autosave needs to feel responsive; other admin screens tick more slowly. Multiply by every open tab and every logged-in editor, and on a small server you can spend a meaningful share of your PHP workers on polling.</p>



<p class="wp-block-paragraph">The proportionate fix is to slow it down rather than kill it:</p>



<pre class="wp-block-code"><code>// Slow the heartbeat down across admin screens.
// Core clamps this to its allowed range, so an out-of-range
// value is quietly ignored rather than applied.
add_filter( 'heartbeat_settings', function ( $settings ) {
    $settings['interval'] = 60;
    return $settings;
} );</code></pre>



<p class="wp-block-paragraph">Worth being honest about the trade-off: at a longer interval, autosave and post-lock detection get less immediate. On a single-author blog that is invisible. On a newsroom where three people edit the same article, post locking becoming less prompt is a genuine cost and you should leave the editor alone and throttle elsewhere.</p>



<p class="wp-block-paragraph">Heartbeat is not the only thing hitting that endpoint. Cart fragments, live search, popup builders, notification pollers and bots all target <code>admin-ajax.php</code>. Look at the <code>action</code> parameter in the request payload to see which plugin is responsible, then deal with that plugin rather than blaming the endpoint.</p>



<h2 class="wp-block-heading">wp-cron riding along on your admin requests</h2>



<p class="wp-block-paragraph">WordPress has no scheduler of its own. It checks for due tasks on page loads and fires them off, which means somebody&#8217;s page view pays for your backup job, your feed import and your email queue. On a low-traffic site the person unlucky enough to be that page view is usually you, logged in, clicking around the admin.</p>



<pre class="wp-block-code"><code># What is scheduled, and is anything badly overdue?
wp cron event list

# Run everything due right now, so you can time it in isolation.
wp cron event run --due-now</code></pre>



<p class="wp-block-paragraph">A long list of overdue events means cron is not firing reliably, which is its own problem. The fix is to take the scheduler off page loads and give it to the operating system:</p>



<pre class="wp-block-code"><code>// wp-config.php
define( 'DISABLE_WP_CRON', true );</code></pre>



<pre class="wp-block-code"><code># crontab -e, as the user that owns the WordPress files.
# Every five minutes is enough for most sites.
*/5 * * * * cd /var/www/example.com &amp;&amp; wp cron event run --due-now &gt;/dev/null 2&gt;&amp;1</code></pre>



<p class="wp-block-paragraph">The order matters. Set the constant and the system cron in the same change, because if you disable wp-cron and forget the cron job, scheduled posts stop publishing and nobody notices for a week.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Database bloat, and the list tables that trip over it</h2>



<p class="wp-block-paragraph">Admin list screens do work the front end never does. Counting posts by status for those &#8220;All | Published | Draft&#8221; links, joining postmeta for sortable columns, building filter dropdowns from distinct values. All of it scales with table size, and none of it is cached.</p>



<pre class="wp-block-code"><code># Which tables are actually large?
wp db size --tables --human-readable</code></pre>



<p class="wp-block-paragraph">What usually turns up:</p>



<ul class="wp-block-list">
<li><strong><code>wp_postmeta</code> far larger than <code>wp_posts</code>.</strong> Normal to a point. Suspicious when the ratio is extreme, which usually means a plugin writing meta per page view.</li>
<li><strong>Revisions.</strong> Every save keeps a full copy of the post. Cap them with <code>define( 'WP_POST_REVISIONS', 10 );</code> in wp-config, which limits new ones without touching existing content.</li>
<li><strong>Action Scheduler tables</strong> on WooCommerce sites. Completed actions accumulate. There is a cleanup screen under WooCommerce tools, and it is worth checking before you assume the store itself is slow.</li>
<li><strong>Spam and trashed comments</strong> still counted by every dashboard widget that shows a comment count.</li>
</ul>



<p class="wp-block-paragraph">Two quick wins that need no database work at all. Use Screen Options at the top right of any list table to drop the per-page count from twenty to ten on a slow screen. And switch the Media Library from grid view to list view, because the grid loads far more per scroll.</p>



<h2 class="wp-block-heading">The server underneath</h2>



<p class="wp-block-paragraph">If the application is clean and the admin is still slow, look down a layer. Site Health covers most of it, under Tools then Site Health then Info.</p>



<ul class="wp-block-list">
<li><strong>No persistent object cache.</strong> Without one, every options lookup and every transient read goes to MySQL on every request. Adding Redis or Memcached with the matching drop-in is often the single biggest improvement available to an admin-heavy site, because the admin is exactly the workload that benefits.</li>
<li><strong>OPcache disabled.</strong> PHP recompiling every WordPress file on every request. Free to enable, immediately noticeable.</li>
<li><strong>An old PHP version.</strong> Upgrading a WordPress site&#8217;s PHP is usually the cheapest performance work available, and it is a security matter regardless.</li>
<li><strong>PHP-FPM worker exhaustion.</strong> If requests are queuing for a worker, every page waits, and the admin waits longest because it holds a worker for the whole request. Check the pool status and the error log before adding more workers, because a worker shortage is often a symptom of slow requests rather than the cause.</li>
<li><strong>A low memory limit.</strong> The admin needs more than the front end, especially plugin and update screens.</li>
</ul>



<p class="wp-block-paragraph">If you run your own server, track admin response time the same way you track front-end response time. Scraping PHP-FPM and MySQL metrics into Prometheus and graphing them in Grafana costs an afternoon and turns &#8220;the dashboard feels slower lately&#8221; into something you can actually see.</p>



<h2 class="wp-block-heading">Troubleshooting</h2>



<h3 class="wp-block-heading">Only one admin screen is slow</h3>



<p class="wp-block-paragraph">That points at a specific plugin, not the platform. Query Monitor on that exact screen will name the caller. Common culprits are the plugins list (update checks for every installed plugin) and any settings page that fetches remote data to populate a dropdown.</p>



<h3 class="wp-block-heading">Slow only for some users</h3>



<p class="wp-block-paragraph">Check user meta. Some plugins store per-user data such as dismissed notices, column preferences or activity logs, and one user&#8217;s row can grow enormous. Also check whether the slow users are administrators, since admins load update checks and notices that editors do not.</p>



<h3 class="wp-block-heading">Fast for a while after clearing cache, then slow again</h3>



<p class="wp-block-paragraph">Something is rebuilding a large transient or option on a schedule and it is expensive. Watch <code>wp_options</code> row sizes over a day, or check the cron list for a job that lines up with the slowdown.</p>



<h3 class="wp-block-heading">Slow in one browser or on one machine only</h3>



<p class="wp-block-paragraph">Not a server problem. Test in a private window with extensions disabled. Ad blockers and password managers interact badly with some admin screens, and a stale service worker from a caching plugin can serve half-broken admin assets.</p>



<h3 class="wp-block-heading">Everything looks fine but it is still slow</h3>



<p class="wp-block-paragraph">Bisect on staging, never production. Deactivate all plugins, confirm the admin is fast, then reactivate in halves rather than one at a time. Switch to a default theme too, because <code>functions.php</code> in a commercial theme can do as much work as a plugin.</p>



<h2 class="wp-block-heading">Common mistakes</h2>



<p class="wp-block-paragraph">Things I would push back on if you suggested them:</p>



<ul class="wp-block-list">
<li>Judging admin performance by front-end speed test scores. Those measure a cached HTML file.</li>
<li>Installing a second caching plugin because the first one &#8220;did not fix the dashboard&#8221;. No page cache will ever cache wp-admin.</li>
<li>Running an <code>UPDATE</code> against the <code>autoload</code> column directly, then wondering why nothing changed. The object cache still holds the old values.</li>
<li>Only counting <code>autoload = 'yes'</code> and concluding the table is fine.</li>
<li>Disabling the heartbeat completely, then losing work when two people edit the same post.</li>
<li>Setting <code>DISABLE_WP_CRON</code> without adding a real cron job.</li>
<li>Deleting rows from <code>wp_options</code> without a backup, or without checking on staging first.</li>
<li>Leaving <code>WP_HTTP_BLOCK_EXTERNAL</code> in place because it made the dashboard fast. It also stopped your security updates.</li>
<li>Adding PHP-FPM workers to hide slow requests instead of finding out why they are slow.</li>
<li>Deactivating plugins one at a time on a live site during business hours.</li>
</ul>



<h2 class="wp-block-heading">Best practices</h2>



<ul class="wp-block-list">
<li>Measure with Query Monitor before changing anything, and measure again with it deactivated.</li>
<li>Check autoloaded option size whenever you install, remove or update a major plugin.</li>
<li>Remove uninstalled plugins&#8217; leftover rows as part of routine maintenance, not as an emergency.</li>
<li>Run a persistent object cache on any site where people spend real time in the admin.</li>
<li>Move wp-cron to a system cron job on every site you manage.</li>
<li>Cap post revisions rather than deleting them in bulk later.</li>
<li>Keep a staging copy so bisecting plugins never happens on production.</li>
<li>Treat Site Health warnings as a starting point for investigation, not a checklist to silence.</li>
<li>Uninstall plugins properly instead of deactivating them and leaving them installed, since update checks still run for installed plugins.</li>
<li>Watch admin response time over time, so you catch a slow regression before somebody files a ticket.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">FAQ</h2>



<h3 class="wp-block-heading">Why is my WordPress dashboard slow when the site itself is fast?</h3>



<p class="wp-block-paragraph">Because the front end is being served from a cache and the dashboard cannot be. Page caches and CDNs bypass logged-in requests by design, so wp-admin runs the full PHP and MySQL stack on every click. The dashboard is showing you your site&#8217;s real uncached performance.</p>



<h3 class="wp-block-heading">Will a caching plugin speed up wp-admin?</h3>



<p class="wp-block-paragraph">Not the page caching part, no. What can help is the object caching side, if your caching plugin ships one, because that caches option and transient lookups rather than whole pages. Redis or Memcached with the matching drop-in does the same job more reliably.</p>



<h3 class="wp-block-heading">How much autoloaded data is too much?</h3>



<p class="wp-block-paragraph">Core&#8217;s Site Health check will tell you when you have crossed its threshold, and that is the number to work against rather than one from an article. More usefully: if a single option is measured in hundreds of kilobytes, look at it regardless of the total, because one bad row is easier to fix than a general diet.</p>



<h3 class="wp-block-heading">Is it safe to delete rows from wp_options?</h3>



<p class="wp-block-paragraph">With a backup and a staging test, yes for orphaned rows from plugins you have removed. For anything belonging to an active plugin, turn autoload off instead of deleting, and use <code>wp_set_option_autoload()</code> rather than raw SQL so the object cache stays consistent.</p>



<h3 class="wp-block-heading">Should I disable the Heartbeat API?</h3>



<p class="wp-block-paragraph">Throttle it, do not disable it. Turning it off entirely breaks autosave and post locking, which is a bad trade for a site with more than one editor. Raising the interval keeps both working and removes most of the load.</p>



<h3 class="wp-block-heading">Does the number of plugins matter?</h3>



<p class="wp-block-paragraph">Less than people assume. Twenty well-written plugins can cost less than one that runs a remote API call on <code>admin_init</code>. Count what each plugin does per request, not how many are in the list. Query Monitor attributes queries and HTTP calls to specific plugins, which turns this from a guess into a measurement.</p>



<h3 class="wp-block-heading">Will better hosting fix a slow WordPress admin?</h3>



<p class="wp-block-paragraph">It helps when the constraint is genuinely the server: no OPcache, no object cache, throttled CPU, a shared MySQL instance under load. It does nothing for four megabytes of autoloaded options or a plugin blocking on a remote API. Diagnose first, because moving a slow site to a faster server usually gives you a slightly less slow site and a new bill.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The one thing to remember</h2>



<p class="wp-block-paragraph">Your dashboard is not a separate, badly built part of WordPress. It is the only place you ever see your site running without a cache in front of it, which makes it the most honest performance signal you have. A slow WordPress admin is telling you something true about the application, and the front end has been politely covering for it.</p>



<p class="wp-block-paragraph">So measure before you change anything, and go in this order: autoloaded options, outbound HTTP requests, admin-ajax traffic, cron, database size, then the server. That sequence puts the highest-yield, lowest-risk checks first, and most of the time you will not get past the second one.</p>



<h2 class="wp-block-heading">Need someone to find where the time is going?</h2>



<p class="wp-block-paragraph">Diagnosing a slow admin is mostly measurement discipline, and it goes faster with someone who has looked at a lot of these. Work I take on regularly:</p>



<ul class="wp-block-list">
<li>Profiling a slow WordPress admin and reporting back with the actual cause, named, rather than a list of generic tips.</li>
<li>Cleaning up <code>wp_options</code> and autoloaded data safely, tested on staging before it touches production.</li>
<li>Setting up Redis or Memcached object caching, OPcache and PHP-FPM tuning on a VPS or dedicated server.</li>
<li>Moving wp-cron to system cron and fixing scheduled jobs that have quietly stopped running.</li>
<li>Database maintenance: revisions, orphaned meta, Action Scheduler cleanup on WooCommerce sites, and indexing where it genuinely helps.</li>
<li>Adding proper monitoring so admin performance regressions show up on a graph instead of in a complaint.</li>
</ul>



<p class="wp-block-paragraph">Send me a Site Health info report and a Query Monitor screenshot from your slowest admin screen, and I will tell you what I would look at first.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Work with me on Upwork</a></div>
</div>
<p>The post <a href="https://john-nessime.com/blog/wordpress/slow-wordpress-admin/">Your Site Is Fast and Your Dashboard Is Not: Fixing a Slow WordPress Admin</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://john-nessime.com/blog/wordpress/slow-wordpress-admin/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
