WordPress wp_cron.php causing high CPU usage: Proper CLI systemd timer setup

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).

See also  Rank Math SEO schema warning: Missing field 'hasCourseInstance' on custom post type

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.phar or /usr/bin/wp instead of /usr/local/bin/wp, systemd fails with status=203/EXEC. Always verify location using which wp.
  • Incorrect User Permissions: Running wp-cron.service as root creates cache directories or log entries owned by root, causing permission denied errors when web workers (www-data) later try to access wp-content/uploads or 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 include After=mysqld.service or After=mariadb.service.
  • Multisite Network Isolation: On WordPress Multisite, wp cron event run only executes tasks for the primary site by default. Multisite setups require adding --url parameters or using a loop shell script (wp site list --field=url | xargs -n1 wp cron event run --due-now --url=).
See also  Fixing WooCommerce cart session dropping on checkout submission with Redis cache

Leave a Comment