ultimate-multisite / the-perfect-wp-cron
Event-loop job queue for WordPress. Executes WP Cron and Action Scheduler jobs at exact scheduled times with zero polling.
Package info
github.com/Ultimate-Multisite/the-perfect-wp-cron
Type:wordpress-plugin
pkg:composer/ultimate-multisite/the-perfect-wp-cron
Requires
- php: >=8.1
README
Event-loop job queue for WordPress. Replaces WP-Cron's poll-on-visit model and system cron's once-per-minute limitation with a long-running Workerman process that executes every WP Cron event and Action Scheduler action at its exact scheduled time — zero polling, zero delay.
Who Is It For
- Multisite operators running dozens or hundreds of sites where missed cron events and overlapping runners are a constant problem.
- Hosting providers who need predictable, observable background processing without per-site cron entries.
- Sites with heavy Action Scheduler workloads (WooCommerce Subscriptions, background imports, bulk email) that need parallel execution with per-job timeouts.
- Anyone who needs precise scheduling — if a job is scheduled for 14:32:07, it runs at 14:32:07, not whenever the next visitor arrives or the next minute ticks over.
Advantages Over WP-Cron
| WP-Cron | The Perfect WP Cron |
|---|---|
| Triggers on page visits — low-traffic sites miss schedules | Triggers at exact scheduled time via event-loop timer |
| Adds latency to a visitor's request | Runs in a separate process — zero impact on web requests |
| Single-threaded — one job at a time | Configurable parallel workers and concurrency |
| No timeout protection | Per-job SIGALRM timeout stops runaway jobs |
| No visibility into what ran or failed | Admin dashboard + per-job log table with duration and errors |
Advantages Over System Cron
System Cron (* * * * *) |
The Perfect WP Cron |
|---|---|
| Minimum 1-minute granularity | Sub-second precision via Workerman timers |
| Polls the database every minute even if nothing is due | Socket notification — the worker knows instantly when a new job is scheduled |
| One WP bootstrap per cron run | Batches jobs by site — multiple jobs share one WP bootstrap |
| Separate cron entry per site (multisite) | Single process handles all sites in the network |
| No built-in concurrency | Configurable worker count and max concurrent subprocesses |
| No automatic restart | Uptime + memory watchdog, designed for systemd auto-restart |
Disadvantages
- Requires CLI/SSH access. You need to run a long-lived PHP process, typically via systemd. Shared hosting without shell access won't work.
- Linux only. Workerman requires
pcntl_forkandpcntl_signal, which are not available on Windows or macOS in production. - Workerman dependency. Adds ~200KB to your vendor directory. The process must be managed (started, monitored, restarted) outside of WordPress.
- More complex than default cron. There's a process to monitor. If it stops unexpectedly and systemd isn't configured, jobs won't run until someone notices.
- Socket communication. The web server's PHP process must be able to write to the Unix socket. File permissions matter.
Requirements
- PHP 8.1+
pcntlextension (standard on Linux, verify withphp -m | grep pcntl)- Linux (Workerman uses
pcntl_fork) - WordPress 6.0+
- Composer
Installation
Via Composer (recommended)
Add the repository and require the package:
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/Ultimate-Multisite/the-perfect-wp-cron.git"
}
],
"require": {
"ultimate-multisite/the-perfect-wp-cron": "dev-main"
}
}
Then run:
composer update ultimate-multisite/the-perfect-wp-cron
The plugin installs to wp-content/plugins/the-perfect-wp-cron/ (or web/app/plugins/ on Bedrock).
Manual Installation
Clone into your plugins directory and install the Workerman dependency:
cd wp-content/plugins git clone https://github.com/Ultimate-Multisite/the-perfect-wp-cron.git cd the-perfect-wp-cron composer install --no-dev
Activate the plugin in wp-admin (or network-activate on multisite).
Configuration
Every setting can be configured via PHP constant (in wp-config.php) or environment variable. Constants take priority over env vars. All settings have sensible defaults — zero configuration is required to get started.
| Constant / Env Var | Default | Description |
|---|---|---|
QUEUE_WORKER_SOCKET_PATH |
/tmp/the-perfect-wp-cron.sock |
Unix socket path |
QUEUE_WORKER_COUNT |
2 |
Number of worker processes (Workerman forks) |
QUEUE_WORKER_MAX_CONCURRENT |
1 |
Max concurrent subprocesses per worker |
QUEUE_WORKER_MAX_BATCH_SIZE |
50 |
Max jobs per subprocess batch |
QUEUE_WORKER_JOB_TIMEOUT |
300 |
Per-job timeout in seconds (SIGALRM) |
QUEUE_WORKER_BATCH_TIMEOUT |
3600 |
Subprocess timeout in seconds (safety net) |
QUEUE_WORKER_RESCAN_INTERVAL |
60 |
Seconds between database rescans |
QUEUE_WORKER_MEMORY_LIMIT |
200 |
Memory limit in MB before auto-restart |
QUEUE_WORKER_UPTIME_LIMIT |
3600 |
Max uptime in seconds before auto-restart |
QUEUE_WORKER_LOG_FILE |
auto-detect | Path to log for admin viewer |
QUEUE_WORKER_LOG_RETENTION |
7 |
Days to keep job log entries |
DOMAIN_CURRENT_SITE |
localhost |
Primary domain for WP bootstrap in worker |
WP_ROOT_PATH |
auto-detect | Path to directory containing wp-load.php |
Example wp-config.php:
define('QUEUE_WORKER_SOCKET_PATH', '/run/the-perfect-wp-cron.sock'); define('QUEUE_WORKER_COUNT', 4); define('QUEUE_WORKER_JOB_TIMEOUT', 600); define('QUEUE_WORKER_LOG_FILE', '/var/log/the-perfect-wp-cron.log');
Usage
Starting the Worker
# Foreground (for debugging) php wp-content/plugins/the-perfect-wp-cron/bin/worker.php start # Daemonized php wp-content/plugins/the-perfect-wp-cron/bin/worker.php start -d # Stop / Restart / Status php wp-content/plugins/the-perfect-wp-cron/bin/worker.php stop php wp-content/plugins/the-perfect-wp-cron/bin/worker.php restart php wp-content/plugins/the-perfect-wp-cron/bin/worker.php status
Bedrock: replace wp-content/plugins with web/app/plugins.
systemd Service (Production)
Create /etc/systemd/system/the-perfect-wp-cron.service:
[Unit] Description=WordPress Queue Worker After=network.target mariadb.service [Service] Type=simple User=www-data Group=www-data WorkingDirectory=/var/www/example.com/current ExecStart=/usr/bin/php web/app/plugins/the-perfect-wp-cron/bin/worker.php start Restart=always RestartSec=5 StandardOutput=append:/var/log/the-perfect-wp-cron.log StandardError=append:/var/log/the-perfect-wp-cron-error.log MemoryMax=1G Environment=WP_ENV=production Environment=QUEUE_WORKER_COUNT=4 [Install] WantedBy=multi-user.target
sudo systemctl enable the-perfect-wp-cron
sudo systemctl start the-perfect-wp-cron
WP-CLI Commands
wp queue status # Show worker PID, uptime, memory, pending/running jobs wp queue populate # Rescan — send all pending jobs to the worker wp queue restart # Graceful restart (systemd auto-restarts)
Admin Dashboard
After activating the plugin, a Queue Worker page appears under:
- Network Admin > Settings (multisite)
- Tools (single site)
The dashboard shows:
- Worker Status — running/stopped, PID, uptime, memory, currently executing jobs. Auto-refreshes every 10 seconds.
- Per-Site Resource Usage — which sites consume the most CPU time over the last 24 hours.
- Job History — searchable, filterable, sortable log of every executed job with status, duration, and error messages.
- Recent Log Entries — tail of the worker log.
Architecture
WordPress Request Worker Process (Workerman)
+---------------------------+ +------------------------------------+
| Cron_Interceptor | | Event loop (libevent/select) |
| hooks schedule_event |------->| Unix socket listener |
| | socket | Timer per job (exact timestamp) |
| Action_Scheduler_Bridge | | Periodic DB rescan (safety net) |
| hooks stored_action |------->| Memory + uptime watchdog |
+---------------------------+ +------------------------------------+
|
Timer triggers at scheduled time
|
Claim job (INSERT IGNORE lock)
|
Batch by site_id
|
+------------------------------+
| Subprocess: execute-job.php |
| Bootstrap WP for site |
| For each job in batch: |
| SIGALRM timeout guard |
| Run hook / AS action |
| Log result to qw_job_log |
+------------------------------+
Flow:
- WordPress schedules a cron event or Action Scheduler action.
- The plugin intercepts the schedule call and sends a JSON payload to the worker via Unix socket.
- The worker sets a Workerman timer for the job's exact timestamp.
- When the timer triggers, the worker atomically claims the job via
INSERT IGNOREinto a lock table (prevents duplicate execution across workers). - Claimed jobs are batched by
site_idand flushed to a subprocess every second. - The subprocess (
execute-job.php) bootstraps WordPress for the target site's domain, executes each job with a per-job SIGALRM timeout, logs results to theqw_job_logtable, and exits. - The worker polls subprocesses for completion and logs batch results.
- A periodic database rescan catches any jobs that arrived before the worker started or bypassed socket notification.
Troubleshooting
Worker won't start — "Address already in use"
A leftover socket exists. The worker tries to clean it up automatically, but if another process holds it: rm /tmp/the-perfect-wp-cron.sock (or your configured path).
Jobs aren't executing
- Check
wp queue status— is the worker running? - Check
wp queue populate— does it find pending jobs? - Check the worker log for errors.
- Verify the web server user can write to the socket path.
"Could not find wp-load.php"
Set the WP_ROOT_PATH environment variable to the directory containing wp-load.php (for standard WP) or web/wp/wp-load.php's parent (Bedrock auto-detected).
Socket permission denied
The worker creates the socket with mode 0660. Ensure the web server user (www-data) and the worker process user are in the same group, or configure the socket path to a directory both can access.
Per-job timeout stops a legitimate long-running job
Increase QUEUE_WORKER_JOB_TIMEOUT (default 300 seconds). For specific hooks that need more time, consider breaking the work into smaller chunks.
High memory usage / frequent restarts
The watchdog restarts workers when memory exceeds QUEUE_WORKER_MEMORY_LIMIT (default 200 MB) or uptime exceeds QUEUE_WORKER_UPTIME_LIMIT (default 3600 seconds). These are safety nets — increase them if your workload legitimately needs more resources, or investigate memory leaks in the jobs themselves.
License
GPL-2.0-or-later. See LICENSE.