WP-Cron изнутри: как работает планировщик задач WordPress
Задача на 03:00 выполнилась в 03:47, потому что в WP-Cron нет демона: расписание проверяется только при визите на сайт. Разбираем хранение событий, точки отказа и переход на системный cron.

Задача назначена на 03:00, а в логе появилась в 03:47. Ночью на сайт никто не заходил, и до первого посетителя WordPress не знал, что пора что-то запускать. Это не сбой, а устройство WP-Cron: фонового демона нет, расписание проверяется только при обращении к сайту. Разбираем механизм в WordPress 6.x, места, где он ломается, и переход на системный cron.
Где живёт расписание
Все события лежат в одной записи таблицы wp_options с ключом cron. Это сериализованный массив: метка времени, под ней имя хука, под ним хеш аргументов, а внутри параметры события (schedule, args, interval у повторяющихся). Отдельной таблицы нет, как нет индексов по времени и статусов вроде «выполняется» или «упало».
Из этого следуют три вещи:
- чтобы найти просроченное, WordPress читает и разбирает весь массив, сколько бы в нём ни было событий;
- любое изменение расписания перезаписывает запись целиком, и по умолчанию без блокировки: два параллельных процесса могут затереть правки друг друга;
- у задачи нет истории. Выполнилась она, упала или не стартовала, из самого WordPress не узнать.
Что происходит при обращении к сайту
- На хуке
initвызываетсяwp_cron(). Он читает массив и ищет событие со временем в прошлом. - Если такое есть,
spawn_cron()проверяет блокировкуdoing_cron. Она живётWP_CRON_LOCK_TIMEOUTсекунд, по умолчанию 60. - WordPress шлёт неблокирующий loopback-запрос на
wp-cron.php?doing_wp_cron=...(пояснение в конце) и отдаёт страницу посетителю, не дожидаясь ответа. - Отдельный процесс
wp-cron.phpставит блокировку, перебирает события по времени и вызывает хуки один за другим. - У повторяющегося события следующий запуск назначается до вызова хука. Если колбэк упадёт с фатальной ошибкой, расписание сохранится, но этот запуск не повторится. Одноразовое событие снимается тоже до вызова, поэтому при фатальной ошибке оно просто пропадёт.
WP-Cron гарантирует только одно: задача не запустится раньше срока. Когда именно она запустится, зависит от того, кто и когда откроет сайт.
Три режима запуска
| Режим | Что запускает | Точность | Цена |
|---|---|---|---|
| По умолчанию | Посетитель сайта через loopback-запрос | Не раньше срока; опоздание равно паузе между визитами | Проверка расписания на каждой загрузке; не работает без трафика и за закрытым loopback |
DISABLE_WP_CRON и системный cron | Планировщик ОС, обычно каждые 1-5 минут | До интервала в crontab | Нужен доступ к crontab или панели хостинга и свой мониторинг |
ALTERNATE_WP_CRON | Посетитель, через редирект с параметром в том же запросе | Как у режима по умолчанию | Лишний редирект для GET-запросов; нужен, только если loopback недоступен |
Где подход ломается
Нет посетителей или кеш отдаёт страницы без PHP
Полностраничный кеш (плагин или кеш на уровне Nginx) отдаёт готовый HTML, и WordPress до хука init не доходит. Расписание ждёт, пока кто-то промахнётся мимо кеша. Первыми страдают записи со статусом «Запланировано», очереди писем и плановые очистки.
Закрытый loopback
Basic-аутентификация на всём сайте, фильтр по IP, правило WAF или хостинг, который запрещает соединения на собственный домен: запрос до wp-cron.php не доходит. Ошибки вы не увидите, потому что запрос неблокирующий. Признак: предупреждение о loopback-запросе или о запоздавших событиях в разделе «Инструменты - Здоровье сайта».
Тяжёлая задача и раздутый массив
Просроченные события выполняются последовательно в одном процессе под лимитом max_execution_time. Долгий импорт задерживает все остальные, а при превышении лимита процесс обрывается. Отдельная беда: плагин, который создаёт события с уникальными аргументами (номер заказа, URL) и не удаляет их. Запись cron растёт, и каждая проверка разбирает её целиком.
Ещё два частых промаха. Вызов wp_schedule_event() на каждой загрузке без проверки wp_next_scheduled() плодит дубли у повторяющихся событий. А wp_schedule_single_event() молча игнорирует событие, если событие с тем же хуком и аргументами уже стоит в пределах 10 минут (защита от дублей появилась в WordPress 5.1).
Что и как мерить
Замеров на вашем железе у меня нет, а придумывать цифры нельзя, поэтому вот метод. Сначала снимите состояние до любых изменений, потом повторите после перехода на системный cron и сравните.
# Сколько событий в расписании
wp cron event list --format=count
# Что просрочено: смотрите колонку next_run_relative
wp cron event list --fields=hook,next_run_relative,recurrence
# Размер записи cron в байтах (префикс wp_ замените на свой)
wp db query "SELECT LENGTH(option_value) AS bytes FROM wp_options WHERE option_name = 'cron'"
# Может ли сайт вообще запускать cron
wp cron test
# Сколько занимает прогон просроченных задач (лучше на стенде)
time wp cron event run --due-now
Фиксируйте условия: версию WordPress и PHP, число активных плагинов, наличие кеша, объём трафика. Без них цифры сравнивать нельзя.
Переход на системный cron
Сначала отключите запуск по визитам в wp-config.php, выше строки «That's all, stop editing»:
<?php
// WordPress 6.x: cron больше не запускается при обращении к сайту
define( 'DISABLE_WP_CRON', true );
Затем добавьте задачу в crontab пользователя, от которого работает сайт. Путь к wp проверьте через which wp: у cron урезанный PATH. Обёртка flock не даёт запускам наложиться друг на друга.
*/5 * * * * flock -n /tmp/wp-cron-example.lock /usr/local/bin/wp cron event run --due-now --path=/var/www/example.com/public >> "$HOME/wp-cron.log" 2>&1
Свои задачи в плагине планируйте так, чтобы повторный запуск был безопасен:
<?php
// WordPress 6.x. Фрагмент главного файла плагина.
// Своя периодичность: ключ используется при планировании
add_filter( 'cron_schedules', function ( $schedules ) {
$schedules['every_fifteen_minutes'] = array(
'interval' => 15 * MINUTE_IN_SECONDS,
'display' => 'Каждые 15 минут',
);
return $schedules;
} );
// Планируем один раз, а не на каждой загрузке страницы
add_action( 'init', function () {
if ( ! wp_next_scheduled( 'myplugin_sync' ) ) {
wp_schedule_event( time(), 'every_fifteen_minutes', 'myplugin_sync' );
}
} );
add_action( 'myplugin_sync', 'myplugin_run_sync' );
function myplugin_run_sync() {
// Работа задачи. Запуск может повториться, код должен это пережить.
}
// При деактивации убираем события, иначе они останутся в расписании
register_deactivation_hook( __FILE__, function () {
wp_clear_scheduled_hook( 'myplugin_sync' );
} );
Что делать дальше
- Через сутки после перехода выполните
wp cron event listи убедитесь, что просроченных событий нет. - Настройте оповещение: самое старое просроченное событие старше двух интервалов crontab значит, что планировщик встал.
- Найдите в списке хуки удалённых плагинов. Они остаются в расписании и копят мусор.
- Следите за размером записи
cron: рост между замерами указывает на плагин, который не чистит свои события.
Очереди с тысячами задач и повторными попытками в WP-Cron не лечатся. Библиотека Action Scheduler с собственными таблицами решает эту задачу, но это отдельная тема.
Loopback-запрос: HTTP-запрос, который сайт отправляет сам себе по своему же адресу. Для WP-Cron это способ запустить wp-cron.php отдельным процессом, не задерживая посетителя.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: WordPress Plugin Handbook: Cron, WordPress Plugin Handbook: Hooking WP-Cron Into the System Task Scheduler
Частые вопросы
Почему задача выполнилась не в назначенное время?
Что будет, если задать DISABLE_WP_CRON и не настроить системный cron?
Как понять, что WP-Cron сломан?
А если у меня другая версия WordPress или команда не сработала?
Можно ли ставить системный cron каждую минуту и что с мультисайтом?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.