Cron Monitor is a WP-Cron log, monitor and event manager for WordPress. It records every scheduled
event as it runs — not just what is scheduled — so you can see how long a cron job took, how much
memory it used, how many database queries it made, which callbacks were listening, what warnings it
raised, and whether it finished at all.
WordPress ships no cron log. The schedule is a list of hook names and timestamps, and once an event
fires, nothing is written down. That is why a cron job that runs twice, dies halfway, or silently
stops is so hard to pin on anything. Cron Monitor writes the record WordPress does not.
wp-cron.php, a stall check, and a recommended WP_CRON_LOCK_TIMEOUT.wp cronmon runs, wp cronmon health, wp cronmon orphans) and a REST API (/cronmon/v1/health, /cronmon/v1/runs) for external monitoring.Cron Monitor resolves each registered callback back to the plugin, theme, mu-plugin or core file
that declared it — by reflection, at the moment the event runs. No guessing from the hook name.
A hook with no callbacks looks identical whether the plugin that owned it was deleted, or whether it
registers its callback only during cron. Tools that read the hook list from an admin request see
“None” either way. Cron Monitor watches the event actually run, so it can tell you the difference:
Events it has not observed yet are reported as exactly that. It never recommends deleting something
it has not watched.
wp_schedule_event() has no duplicate check of any kind — the single-event function has a ±10
minute one, the recurring one has none at all. Plugins that re-schedule on every activation quietly
stack up copies. Cron Monitor groups recurring events on hook, arguments and schedule, and
shows a dry-run list before removing anything. One-off events are excluded from automatic removal:
the same callback may be intentionally scheduled at different times.
The row for an event is written when it starts, not when it finishes, so an event killed by a fatal
error, an out-of-memory or a max_execution_time timeout still leaves a record. Those are the
failures a log written at the end would lose — and they are the ones you needed.
WordPress runs cron under a lock and re-checks that lock after every single event. One slow job going
over WP_CRON_LOCK_TIMEOUT hands the lock to the next process, and everything behind it waits.
Cron Monitor names the job that overran, by how much, and how long the handover took.
Every other alert needs the event to run: a fatal is caught inside the callback, drift is
measured when the late run finally starts. A scheduled event that has simply stopped never
starts, so it never trips anything. Cron Monitor checks the schedule from ordinary page requests
— the requests that still happen when cron does not — and alerts when an event has sat past its
due time across two checks fifteen minutes apart. One check would fire on every quiet site whose
first visitor in an hour is about to run the backlog; two means it genuinely did not run.
The limit is stated plainly: a site that receives no requests at all cannot notice anything from
the inside. For that case, point external monitoring at the REST health route below.
It is autoloaded on every request. WordPress 6.6 added a size limit for autoloaded options — and it
can never apply to this one, for a reason explained on the Health screen. On a site with thousands of
stale single events, that option is on every page load.
Install a plugin and it silently adds six recurring events; deactivate one and its events either
vanish or, worse, stay behind. Cron Monitor keeps track of the recurring schedule, badges events that
appeared in the last week as new, and lists the ones that disappeared — with when.
WP-Cron is interval-based: a daily event runs 86,400 seconds after its last run, which is the same
wall-clock time only until daylight-saving time changes. Pin an event to 03:00 and Cron Monitor
reschedules it in the site timezone instead, so it stays at 03:00 all year. Whole-day intervals only
— there is no such thing as “twice daily at 03:00”.
The event stays scheduled and keeps firing on time; only its callbacks are suppressed, and every
suppressed firing is still recorded as such. A hook paused by another cron plugin is recognised and
reported as paused, rather than as mysteriously interrupted.
Change a scheduled event’s arguments, time or recurrence without losing its pin or its duplicate
guard, add new events, and create custom recurring schedules. Every write is validated first and
refused with a reason, never silently corrected.
Any code running inside a cron callback can attach a note to the run being recorded:
do_action( 'cronmon_log', 'reindexed 412 products' );
It is an action, so it needs no function_exists() guard and no dependency — with Cron Monitor
inactive the call does nothing. Notes appear on the Run History screen with their offset from the
start of the run.
wp cronmon runs --status=fatal, `wp cronmon health` (exits non-zero on a critical finding, so it
sits in a monitoring crontab) and wp cronmon orphans print the same verdicts and findings as the
screens, from the same code.
GET /wp-json/cronmon/v1/health returns the same ranked findings as the Cron Health screen, a
critical count and one `ok` boolean — the field an Uptime Kuma, Zabbix or Better Stack check
compares against. GET /wp-json/cronmon/v1/runs?status=fatal&since=-1%20hour&limit=20 returns
recorded runs with the same filters as wp cronmon runs, timestamps in ISO 8601 UTC.
Both require the manage_options capability; Application Passwords over HTTPS are the intended
way in. Fifty sites, one dashboard, no SSH.
Two custom database tables, per site, prefixed with your table prefix.
cronmon_runs — one row per cron event that ran:
cronmon_hooks — one row per scheduled event: the names of the callbacks last observed on it,
which components own them, and counters for how many times it has run.
Error messages can contain absolute filesystem paths, and — if a plugin puts them there — email
addresses or order references. That is why they are capped, and why arguments are hashed rather than
stored. The argument preview on the Scheduled Events screen is read live from the cron option, where
the values already live, and is never written anywhere. Deleting the plugin drops both tables and
every option and transient it created.
Settings, including the optional alert email address and webhook URL, live in one option,
cronmon_settings. The stopped-running check keeps one short-lived transient and one small option
holding the time of its next check. Everything is removed on uninstall.
No third-party libraries are bundled and nothing is minified: every file ships as its readable source.
Row retention is configurable. Successful runs default to seven days, failures to fourteen, with a
50,000-row cap.
This plugin sends nothing to anyone but you. No phone-home, no analytics, no update checker, no
bundled third-party libraries.
Nothing outbound ever happens on a request you did not initiate. This plugin contacts no
third-party service of its own. It makes at most two outbound HTTP requests, both under your
control, and can send email through your site’s own mail path:
wp-cron.php whether it answers. That request goes to your own site’s address,ALTERNATE_WP_CRON) it does not run at all.wp_mail(), to one address you type in yourself. That is your site’s