WordPress wp-cron.php high CPU usage occurs because default WP-Cron execution is triggered on every HTTP page request, causing heavy database queries and PHP worker starvation under high traffic. When multiple concurrent visitors land on a site, multiple PHP-FPM processes spawn simultaneously to execute the same scheduled tasks, leading to resource locks and CPU spikes. Offloading task execution from HTTP requests to a dedicated Linux systemd timer decouples background cron jobs from web traffic entirely.
Root Cause Diagnosis
Default WP-Cron relies on incoming site traffic: every page load checks if scheduled tasks are due and spawns a non-blocking loopback HTTP request to wp-cron.php. On high-traffic sites or during traffic surges, this results in race conditions where hundreds of parallel PHP processes attempt to process the cron array option in wp_options. The continuous lock contention, database read/write spikes, and PHP-FPM worker saturation cause server CPU utilization to hit 100%.
Quick Fix
Disable pseudo-cron in wp-config.php and run a one-time WP-CLI invocation via system shell:
Bash
# Add cron disable constant to wp-config.php
sed -i "/^\/\* That's all, stop editing! \*\//i define('DISABLE_WP_CRON', true);" /var/www/html/wp-config.php
# Test manual CLI invocation
wp cron event run --due-now --path=/var/www/html --allow-root
Step-by-Step Resolution
1. Disable Default WP-Cron Execution
Explicitly instruct WordPress to stop checking for scheduled tasks on incoming HTTP requests.
Edit /var/www/html/wp-config.php (adjust path for your document root) and insert the constant above the database settings or main comment block:
PHP
define('DISABLE_WP_CRON', true);
2. Create the systemd Service Unit
Define a systemd service that uses WP-CLI to process due cron events as the dedicated web server user (e.g., www-data or nginx).
Create /etc/systemd/system/wp-cron.service:
Ini, TOML
[Unit]
Description=Run WordPress WP-Cron via WP-CLI
After=network.target mariadb.service mysqld.service
[Service]
Type=oneshot
User=www-data
Group=www-data
WorkingDirectory=/var/www/html
ExecStart=/usr/local/bin/wp cron event run --due-now --path=/var/www/html --quiet
[Install]
WantedBy=multi-user.target
3. Create the systemd Timer Unit
Define a systemd timer that triggers the service at a fixed interval (e.g., every 5 minutes).
Create /etc/systemd/system/wp-cron.timer:
Ini, TOML
[Unit]
Description=Trigger WP-Cron Service Every 5 Minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Unit=wp-cron.service
[Install]
WantedBy=timers.target
4. Enable and Verify the systemd Timer
Reload systemd configuration, start the timer, and verify its execution schedule.
Bash
# Reload unit definitions
sudo systemctl daemon-reload
# Enable and start the timer
sudo systemctl enable --now wp-cron.timer
# Check timer status and next execution run
sudo systemctl status wp-cron.timer
# View active timers on system
sudo systemctl list-timers --all | grep wp-cron
Inspect execution logs to verify successful runs without errors:
Bash
journalctl -u wp-cron.service --since "1 hour ago"
Why This Fails for Some Users
- WP-CLI Binary Path Mismatches: If WP-CLI is installed at
~/.wp-cli/builds/gh-pages/phar/wp-cli.pharor/usr/bin/wpinstead of/usr/local/bin/wp, systemd fails withstatus=203/EXEC. Always verify location usingwhich wp. - Incorrect User Permissions: Running
wp-cron.serviceasrootcreates cache directories or log entries owned byroot, causing permission denied errors when web workers (www-data) later try to accesswp-content/uploadsor object cache files. - Database Connection Failures on System Boot: If the timer fires immediately at boot (
OnBootSec) before MySQL/MariaDB is ready to accept connections, WP-CLI throws a database connection error. The service unit must includeAfter=mysqld.serviceorAfter=mariadb.service. - Multisite Network Isolation: On WordPress Multisite,
wp cron event runonly executes tasks for the primary site by default. Multisite setups require adding--urlparameters or using a loop shell script (wp site list --field=url | xargs -n1 wp cron event run --due-now --url=).