The case for never opening wp-admin again
I manage around 30 WordPress sites. If I had to log into each one, click through the admin panel, run updates, check database health, clean up transients - I'd lose an entire day every week. Instead, I open a terminal and handle all of it in minutes.
That's WP-CLI. The official command-line interface for WordPress. It does everything wp-admin does, but faster, scriptable, and without a browser.
But here's the thing most WP-CLI tutorials miss: they list commands like a glossary. "Here's wp plugin list. Here's wp user create." Great - you could get that from wp help. What they don't show is how these commands fit together in real workflows. How you chain them. How you use them to debug a slow site at 2 AM when a client calls. How you build scripts that handle maintenance across dozens of sites while you sleep.
That's what this guide covers.
Installation (30 seconds)
If you don't have WP-CLI yet:
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
Verify it works:
wp --info
On most managed hosts (Kinsta, WP Engine, Cloudways, GridPane), WP-CLI is already installed. SSH in and type wp --version to check.
Site management commands
These are the commands you'll run most often. Updates, installs, config changes.
Updates in one line
wp core update && wp plugin update --all && wp theme update --all
That's core, all plugins, all themes. One line. What would take 15 clicks in wp-admin takes 3 seconds.
But don't be reckless. Back up first:
wp db export ~/backups/$(date +%Y%m%d).sql && wp core update && wp plugin update --all
If something breaks, you've got the SQL dump from 10 seconds ago.
Core integrity check
wp core verify-checksums
This compares every core file against the official WordPress.org checksums. If anything's been modified - whether by a hack, a bad deploy, or a careless edit - it tells you exactly which files. I run this on every new client site before touching anything else.
Config management
# Read a value from wp-config.php
wp config get DB_HOST
# Set a constant
wp config set WP_DEBUG true --raw
# Add a new constant
wp config set DISABLE_WP_CRON true --raw
The --raw flag is important. Without it, WP-CLI wraps the value in quotes, turning true into the string 'true' instead of the boolean true. That's a bug you don't want to debug at midnight.
Plugin and theme management
Diagnosing plugin conflicts
Site broken and you don't know which plugin did it? This is the fastest diagnostic:
wp plugin deactivate --all
Problem gone? Good. Now reactivate one by one:
wp plugin activate plugin-name
Keep going until the problem returns. That's your culprit. The whole process takes 2 minutes from the command line. In wp-admin, you're clicking through confirmation dialogs for 10 minutes, and if the admin panel itself is broken, you can't even get there.
Detailed plugin info
wp plugin list --fields=name,status,version,update,auto_update
This shows you everything at a glance - which plugins are active, which have updates pending, and which have auto-update enabled. I check this before every deployment.
Install and activate in one shot
wp plugin install query-monitor --activate
For a fresh site setup, I'll chain these:
wp plugin install query-monitor wordfence wp-multitool --activate
Theme operations
# Install and activate a theme
wp theme install flavor --activate
# List installed themes
wp theme list --fields=name,status,version,update
# Delete inactive themes (keep your active + one default)
wp theme delete flavor flavor-child
Database operations
This is where WP-CLI really earns its keep. Direct database access without phpMyAdmin, without exposing port 3306, without any GUI.
Raw queries
wp db query "SELECT COUNT(*) as total_posts FROM wp_posts WHERE post_status='publish';"
Simple. Direct. No middleman.
The autoload query I run on every site
wp db query "SELECT SUM(LENGTH(option_value)) as autoload_bytes FROM wp_options WHERE autoload='yes';"
If that number is over 800KB, you've got a performance problem. Every single page load - every request - WordPress loads all autoloaded options into memory. A bloated wp_options table is one of the most common reasons WordPress sites slow down over time, and most developers never think to check it.
Find the worst offenders:
wp db query "SELECT option_name, LENGTH(option_value) as size_bytes FROM wp_options WHERE autoload='yes' ORDER BY size_bytes DESC LIMIT 20;"
I wrote a full deep dive on why autoload bloat kills performance - it's one of the most overlooked issues in WordPress. The short version: plugins dump serialized data into wp_options with autoload='yes', never clean it up, and your site gets slower by the month.
To fix a specific option:
wp option autoload set bloated_option_name off
That stops it from loading on every page. The option still exists - it just gets fetched on demand instead of preloaded. In most cases, the plugin that created it handles this gracefully.
Database maintenance
# Check all tables for errors
wp db check
# Optimize tables (reclaim space, defragment)
wp db optimize
# Get table sizes
wp db size --tables --human-readable
The wp db size command is gold for spotting runaway tables. I've seen wp_options tables at 200MB, wp_postmeta at 500MB. Once you see which tables are bloated, you know where to dig.
Cleaning post revisions
WordPress stores every revision of every post by default. On a site with 500 posts and 5 years of history, that's easily 10,000+ revision rows:
# Count revisions
wp db query "SELECT COUNT(*) FROM wp_posts WHERE post_type='revision';"
# Delete them all (careful - no undo)
wp post delete $(wp post list --post_type=revision --format=ids) --force
Or limit revisions going forward:
wp config set WP_POST_REVISIONS 5 --raw
That caps it at 5 revisions per post. Enough to recover from mistakes, not enough to bloat your database.
Search-replace: the migration command
If you've ever moved a WordPress site from one domain to another by running a raw SQL REPLACE(), you've probably corrupted serialized data. WordPress stores plugin settings, widget configs, and theme options as PHP serialized strings. Change the string length without updating the byte count, and the data becomes unreadable.
wp search-replace handles this correctly. It unserializes the data, makes the replacement, then re-serializes it with the correct byte count.
Basic domain migration
# Always dry-run first
wp search-replace 'http://staging.example.com' 'https://example.com' --all-tables --dry-run
The dry-run output tells you exactly how many replacements would happen in each table. Review it. Make sure nothing looks unexpected.
# Then run it for real
wp search-replace 'http://staging.example.com' 'https://example.com' --all-tables
HTTPS migration
wp search-replace 'http://example.com' 'https://example.com' --all-tables
Staging to production workflow
This is the full workflow I use for every migration:
# 1. Export production DB as backup
wp @prod db export ~/backups/prod-$(date +%Y%m%d).sql
# 2. Export staging DB
wp @staging db export /tmp/staging.sql
# 3. Import staging DB into production
wp @prod db import /tmp/staging.sql
# 4. Search-replace URLs (dry-run first)
wp @prod search-replace 'https://staging.example.com' 'https://example.com' --all-tables --dry-run
# 5. Execute
wp @prod search-replace 'https://staging.example.com' 'https://example.com' --all-tables
# 6. Flush caches
wp @prod cache flush
wp @prod rewrite flush
The @prod and @staging are WP-CLI aliases - I'll cover those in the advanced section.
User management
Emergency admin access
This is the command I use most when a site goes down and the admin panel is inaccessible. SSH in, create a temporary admin:
wp user create tempadmin admin@example.com --role=administrator --user_pass=$(openssl rand -base64 12)
Fix the problem. Then clean up:
wp user delete tempadmin --reassign=1
The --reassign=1 flag transfers any content owned by that user to user ID 1 (usually the main admin). Don't skip this - deleting a user without reassigning can orphan their posts.
Audit admin accounts
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
I run this on every client site during onboarding. You'd be surprised how often there's a "developer" account from 3 years ago that nobody remembers creating. Every unused admin account is a security risk.
Bulk password resets
After a breach (or suspected breach):
wp user list --role=administrator --field=user_login | while read user; do
newpass=$(openssl rand -base64 16)
wp user update "$user" --user_pass="$newpass"
echo "$user: $newpass"
done
Save the output, distribute new passwords securely, done. This takes 10 seconds for 5 admins. Good luck doing that through wp-admin during an active incident.
Cron management
WordPress cron is not real cron. It's "pseudo-cron" - it only runs when someone visits the site. Low-traffic site? Your scheduled tasks might run hours late. Or never.
See what's scheduled
wp cron event list
This shows every scheduled event, when it's due, and how often it recurs. On most sites, you'll see 20-40 events. Some of them are legitimate (checking for updates, sending scheduled posts). Some are leftovers from plugins you deleted years ago.
Run overdue events
wp cron event run --due-now
Test a specific hook
wp cron event run wp_scheduled_delete
Switch to real cron
For production sites, disable WordPress's pseudo-cron and use system cron instead:
wp config set DISABLE_WP_CRON true --raw
Then add a system crontab entry:
*/5 * * * * cd /var/www/yoursite && wp cron event run --due-now --quiet 2>&1
This runs WP-CLI cron every 5 minutes regardless of traffic. Reliable. Predictable. No more missed scheduled posts.
Find orphaned cron events
After deactivating a plugin, its cron events often stick around:
wp cron event list --fields=hook,next_run_relative,recurrence | grep -v "WordPress"
If you see hooks from plugins that aren't active anymore, clean them up:
wp cron event delete old_plugin_sync_hook
Transient management
Transients are WordPress's caching mechanism for temporary data. API responses, query results, feed data. They expire, but WordPress is lazy about cleaning them up.
# Delete only expired transients
wp transient delete --expired
# Delete ALL transients (safe - they regenerate on demand)
wp transient delete --all
# Count total transients
wp transient list --format=count
On WooCommerce sites and sites with heavy API integrations, I've seen 50,000+ expired transient rows sitting in wp_options. That's dead weight in your database, and since many transients are autoloaded, they're dead weight in memory too.
# Check transient overhead specifically
wp db query "SELECT COUNT(*) as transient_count, SUM(LENGTH(option_value)) as total_bytes FROM wp_options WHERE option_name LIKE '%_transient_%';"
Performance debugging from the command line
This is where WP-CLI becomes a diagnostic tool, not just a management tool.
Memory and load time baseline
wp eval 'echo "Memory: " . round(memory_get_peak_usage()/1024/1024, 2) . " MB\n";'
Object cache status
wp cache type
If this returns "Default", you're not using an object cache. That means every page load hits the database for things Redis or Memcached could serve from memory.
List autoloaded options sorted by size
wp db query "SELECT option_name, LENGTH(option_value) as size_bytes FROM wp_options WHERE autoload = 'yes' ORDER BY size_bytes DESC LIMIT 30;"
WP-CLI's wp option list doesn't sort by size natively, so I use the raw SQL query. I run this more than almost any other command when diagnosing slow queries and general performance issues.
Slow plugin detection
Want to know which plugins are slowing down your site? Query Monitor helps in the browser, but from the CLI:
# Time how long WP takes to bootstrap
time wp eval "echo 'loaded';"
# Now deactivate a suspect plugin and time again
wp plugin deactivate heavy-plugin
time wp eval "echo 'loaded';"
If the load time drops significantly, you found your problem. Reactivate and decide what to do about it.
Export options for comparison
# Save current autoloaded options to a file
wp option list --autoload=yes --format=csv > options-before.csv
# After making changes, export again
wp option list --autoload=yes --format=csv > options-after.csv
# Diff them
diff options-before.csv options-after.csv
This is how I track what plugins do to wp_options during activation. Some plugins add 500KB of autoloaded data the moment you activate them. Good to know before you commit to using them in production.
Advanced: aliases, scripts, and automation
WP-CLI aliases
Aliases let you run commands on remote sites from your local machine. Define them in ~/.wp-cli/config.yml:
@prod:
ssh: deploy@production.example.com/var/www/html
@staging:
ssh: deploy@staging.example.com/var/www/html
@dev:
path: /home/user/sites/dev
Now you can run:
wp @prod plugin list
wp @staging db export /tmp/staging-backup.sql
wp @prod core version
No need to SSH in manually, navigate to the site directory, and run the command. One line from your laptop.
Alias groups
@all:
- @prod
- @staging
- @dev
wp @all core version
Checks the WordPress version on all three environments in one command.
Bash aliases for common tasks
Add these to your ~/.bashrc or ~/.zshrc:
# Quick site health check
alias wphealth='wp core verify-checksums && wp plugin list --update=available --format=count && echo "plugins need updates"'
# Backup and update
alias wpupdate='wp db export ~/backups/pre-update-$(date +%Y%m%d-%H%M).sql && wp plugin update --all && wp core update'
# Autoload size check
alias wpautoload='wp db query "SELECT ROUND(SUM(LENGTH(option_value))/1024) as KB FROM wp_options WHERE autoload=\"yes\";"'
# Quick cleanup
alias wpclean='wp transient delete --expired && wp cache flush'
Multi-site maintenance script
Here's the script I actually use for weekly maintenance across all sites:
#!/bin/bash
SITES=("@client1" "@client2" "@client3" "@mysite")
BACKUP_DIR="$HOME/backups/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
for site in "${SITES[@]}"; do
echo "=== Processing $site ==="
# Backup (exports via SSH, pipes to local file)
wp "$site" db export - --quiet > "$BACKUP_DIR/${site#@}.sql"
# Updates
wp "$site" plugin update --all --quiet
wp "$site" core update --minor --quiet
# Cleanup
wp "$site" transient delete --expired --quiet
# Health check
wp "$site" core verify-checksums 2>&1 | grep -v "Success"
# Report autoload size
echo -n " Autoload: "
wp "$site" db query "SELECT ROUND(SUM(LENGTH(option_value))/1024) as KB FROM wp_options WHERE autoload='yes';" --skip-column-names
echo ""
done
echo "Done: $(date)"
I run this every Monday morning. 30 sites, 5 minutes, zero clicks.
Chaining commands with conditionals
# Only update if there are updates available
wp plugin list --update=available --format=count | grep -q "^0$" || wp plugin update --all
# Verify checksums and alert on failure
wp core verify-checksums || echo "CHECKSUM FAILURE on $(wp option get siteurl)" | mail -s "WP Alert" admin@example.com
Real-world workflow: staging to production
Here's the complete workflow I follow for deploying changes from staging to production. Not the simplified version - the actual steps:
# 1. Snapshot production (safety net)
wp @prod db export ~/backups/prod-pre-deploy-$(date +%Y%m%d-%H%M).sql
# 2. Put production in maintenance mode
wp @prod maintenance-mode activate
# 3. Export staging database
wp @staging db export /tmp/staging-deploy.sql
# 4. Import into production
wp @prod db import /tmp/staging-deploy.sql
# 5. Search-replace URLs
wp @prod search-replace 'https://staging.example.com' 'https://example.com' --all-tables --precise
# 6. Sync uploads (rsync, not WP-CLI)
rsync -avz --delete staging.example.com:/var/www/html/wp-content/uploads/ /var/www/html/wp-content/uploads/
# 7. Flush everything
wp @prod cache flush
wp @prod rewrite flush
wp @prod transient delete --all
# 8. Verify
wp @prod option get siteurl
wp @prod core verify-checksums
# 9. Disable maintenance mode
wp @prod maintenance-mode deactivate
The --precise flag on search-replace forces PHP-based replacement for all columns, not just the ones WP-CLI detects as serialized. Slower, but catches edge cases.
Custom WP-CLI commands
WP-CLI is extensible. You can register your own commands that run alongside the built-in ones. This is useful when you have project-specific tasks that you repeat across sites.
The basics:
// In a plugin or mu-plugin file
if (defined('WP_CLI') && WP_CLI) {
WP_CLI::add_command('mycommand', function($args, $assoc_args) {
$count = $assoc_args['count'] ?? 10;
$posts = get_posts([
'numberposts' => $count,
'post_type' => 'post',
'post_status' => 'draft',
]);
if (empty($posts)) {
WP_CLI::success('No orphaned drafts found.');
return;
}
foreach ($posts as $post) {
WP_CLI::line("{$post->ID}: {$post->post_title} (created: {$post->post_date})");
}
WP_CLI::warning(count($posts) . ' orphaned drafts found.');
});
}
wp mycommand --count=20
For anything more complex, use a class-based approach with proper docblock annotations for argument parsing and wp help integration. The WP-CLI Commands Cookbook covers this in detail.
I use custom commands for things like:
- Auditing user roles and capabilities across multisite
- Generating reports on plugin usage patterns
- Running project-specific data migrations that need WordPress context
WP Multitool takes this concept further with 7 built-in subcommands designed for performance auditing:
# Full site health check
wp multitool health
# Database health report (table sizes, overhead, fragmentation)
wp multitool db-health
# Autoload analysis with actionable recommendations
wp multitool autoload --report
# Capture and analyze slow queries
wp multitool slow-queries --threshold=100
The slow query analysis runs EXPLAIN on each query automatically and tells you exactly which indexes to add. That's something I wished existed in core WP-CLI for years before I built it.
Quick reference
Here's every command from this guide in one place, organized by what you're trying to do:
| Task | Command |
|---|---|
| Update everything | wp core update && wp plugin update --all && wp theme update --all |
| Backup database | wp db export backup.sql |
| Verify core files | wp core verify-checksums |
| Check autoload size | wp db query "SELECT SUM(LENGTH(option_value)) FROM wp_options WHERE autoload='yes';" |
| Find biggest options | wp db query "SELECT option_name, LENGTH(option_value) as bytes FROM wp_options WHERE autoload='yes' ORDER BY bytes DESC LIMIT 20;" |
| Migrate domains | wp search-replace 'old.com' 'new.com' --all-tables |
| Create temp admin | wp user create temp temp@x.com --role=administrator |
| Deactivate all plugins | wp plugin deactivate --all |
| Delete expired transients | wp transient delete --expired |
| Run overdue cron | wp cron event run --due-now |
| Check object cache | wp cache type |
| Database table sizes | wp db size --tables --human-readable |
| Optimize database | wp db optimize |
What's next
If you got this far, you don't need more WP-CLI tutorials. You need to start using it daily until the commands become muscle memory. Set up aliases for your sites. Write one maintenance script. Run it next Monday.
If you're curious what your site looks like under the hood before diving in, run a free scan and see what comes up. Autoload bloat, slow queries, missing indexes - the report tells you exactly which commands to run.





Top comments (0)