RE:NODE

WP-Cron или настоящий cron: почему WordPress пропускает публикации

Автор Команда RE:NODE
17 мин чтения


WP-Cron - это не планировщик. Это проверка, которая выполняется при загрузке страниц: каждый раз, когда кто-то запрашивает страницу, WordPress просматривает свой список задач и, если какая-то просрочена, отправляет фоновый запрос к wp-cron.php, чтобы её выполнить. Нет посетителей - нет и задач, а много посетителей - это очень много проверок. Большинству сайтов нужно исправление из трёх шагов: добавить define( 'DISABLE_WP_CRON', true ); в wp-config.php, вызывать wp-cron.php (или wp cron event run --due-now) из настоящего cron каждые пять минут и убедиться через wp cron event list, что очередь разбирается. Остальная часть статьи объясняет, почему это работает, когда это делать не стоит и как выяснить, почему запланированная запись так и осталась с пометкой «Missed schedule».

Как на самом деле работает WP-Cron#

WordPress хранит все запланированные задачи в одной строке базы данных: опции cron в таблице wp_options, сериализованном массиве с ключами в виде Unix-времени. Каждая запись называет хук (имя действия вроде wp_version_check или publish_future_post), его аргументы и, для повторяющихся задач, интервал вроде hourly или daily. Плагины добавляют свои записи через wp_schedule_event() или wp_schedule_single_event().

На каждом запросе, в самом начале загрузки, WordPress вызывает wp_cron(). Эта функция читает опцию cron и задаёт один вопрос: находится ли самая ранняя метка времени в прошлом? Если нет, ничего не происходит, и стоимость ничтожна. Если что-то пора выполнять, она вызывает spawn_cron(), который делает следующее:

  1. Проверяет блокировку - transient doing_cron. Если другой запуск начался меньше WP_CRON_LOCK_TIMEOUT секунд назад (по умолчанию 60), он сдаётся.
  2. Записывает в блокировку текущее время.
  3. Отправляет HTTP POST с сервера самому себе - так называемый loopback-запрос - на https://your-site/wp-cron.php?doing_wp_cron=<timestamp> с тайм-аутом 0,01 секунды и blocking, выставленным в false, чтобы страница посетителя его не ждала.
  4. Новый запрос к wp-cron.php загружает WordPress, проходит по просроченным задачам по порядку, выполняет каждый хук, переназначает повторяющиеся и удаляет одноразовые.
что-то пора?только если поравыполнить, переназначитьПосетительлюбой запрос страницыWordPressпроверяет опцию cronLoopback-запросPOST самому себеwp-cron.phpвыполняет хукиwp_optionsстрока cron
Что может запустить один просмотр страницы

Для своей исходной задачи это умное решение: виртуальный хостинг 2000-х, где большинство пользователей вообще не могли редактировать crontab. Настраивать ничего не нужно, и всё работает на любом хостинге, где PHP может сделать HTTP-запрос к собственному домену. Все слабые места вытекают из того, что спусковой крючок - посетитель.

  • Время приблизительное. Задача, «назначенная на 03:00», выполнится при первом просмотре страницы после 03:00. На тихом сайте это может быть 07:40.
  • Тихие сайты встают. Сайт-визитка с десятью визитами в день выполняет свои ежечасные задачи десять раз в сутки, а не двадцать четыре.
  • Нагруженные сайты проверяют постоянно. Сама проверка дешёвая, но каждый запущенный loopback - это полная загрузка WordPress, и долгие задачи конкурируют с реальными посетителями за PHP-воркеры.
  • Закэшированные страницы не считаются. Визит, отданный из полностраничного кэша, до PHP не доходит, а значит, и WP-Cron не запускает. Чем лучше кэширование, тем реже выполняется cron.
  • Loopback может молча отказать. Если сервер не может достучаться до собственного публичного адреса, на странице не появится никакой ошибки - задачи просто никогда не выполнятся.

Встроенные интервалы и что по ним выполняется#

Ядро регистрирует четыре интервала: hourly, twicedaily, daily и weekly. Плагины добавляют новые через фильтр cron_schedules - поэтому в списке нагруженного сайта встречаются имена вроде every_minute или five_minutes. Свежая установка планирует несколько задач ядра:

ХукИнтервалЧто делает
wp_version_checktwicedailyПроверяет новый релиз WordPress
wp_update_pluginstwicedailyПроверяет обновления плагинов
wp_update_themestwicedailyПроверяет обновления тем
wp_scheduled_deletedailyОчищает корзину через 30 дней
delete_expired_transientsdailyУдаляет истёкшие transient
wp_privacy_delete_old_export_fileshourlyУдаляет старые экспорты персональных данных
wp_site_health_scheduled_checkweeklyЗапускает тесты Site Health

Точные имена и их количество меняются от версии к версии WordPress, так что считайте таблицу типичной, а не исчерпывающей, и проверяйте свой список через wp cron event list. Автоматические фоновые обновления тоже идут через cron, как и запланированные записи: когда вы планируете публикацию, WordPress добавляет одноразовое событие publish_future_post на время записи с её ID в качестве аргумента.

Дальше идут плагины. Плагины резервного копирования планируют свои запуски, SEO-плагины пересобирают карты сайта, плагины безопасности запускают сканирования, а WooCommerce приносит Action Scheduler - собственную очередь, которая использует WP-Cron как один из триггеров и в магазине может держать тысячи ожидающих действий. На сайте с тридцатью плагинами нормально обнаружить шестьдесят и больше событий, а иногда и горстку, оставленную плагинами, которые удалили без уборки за собой.

Регистрация собственной задачи выглядит так. Проверка wp_next_scheduled() важна - без неё каждая загрузка страницы добавляла бы ещё одну копию события.

mu-plugins/nightly-cleanup.php
<?phpadd_action( 'mysite_nightly_cleanup', 'mysite_do_cleanup' );function mysite_do_cleanup() {    // Delete drafts older than 90 days, prune a log table, and so on.}if ( ! wp_next_scheduled( 'mysite_nightly_cleanup' ) ) {    wp_schedule_event( strtotime( 'tomorrow 03:15' ), 'daily', 'mysite_nightly_cleanup' );}

WP-Cron хранит время в виде меток UTC. Время запланированной записи в редакторе показывается в часовом поясе сайта из Settings, General, и WordPress сам переводит одно в другое, так что часовой пояс сервера значения не имеет - а вот просто неверно идущие часы сервера имеют.

DISABLE_WP_CRON: отключаем триггер на загрузке страниц#

У этой константы узкое и часто неправильно понимаемое действие. Она запрещает wp_cron() запускаться на загрузках страниц. Она не отключает запланированные задачи, ничего не удаляет из опции cron, и wp-cron.php по-прежнему выполняет все просроченные задачи, когда его кто-то запрашивает. Вы убираете триггер, а не планировщик, и поэтому константа - только половина изменения. Вторая половина - дать WordPress новый триггер.

wp-config.php
define( 'DISABLE_WP_CRON', true );/* That's all, stop editing! Happy publishing. */

Ставьте её выше строки «stop editing». Всё, что стоит после загрузки wp-settings.php, не действует, и это самая частая причина жалоб на то, что константа «не работает».

Когда WP-Cron лучше не трогать

Не каждому сайту это нужно. Небольшой блог с ровным трафиком, без полностраничного кэша перед PHP и без задач, чувствительных ко времени, прекрасно живёт на настройках по умолчанию, а системный cron - ещё одна вещь, которая сломается при переезде на другой хостинг. Изменение имеет смысл, если верно что-то из этого:

  • Запланированные записи или письма должны выходить вовремя, а не «где-то после».
  • Трафик низкий или неравномерный, и задачи опаздывают на часы.
  • Большую часть запросов отдаёт полностраничный кэш или CDN, так что PHP редко видит посетителя.
  • Сайт нагружен, и вы хотите убрать работу cron с пути запроса, подальше от реальных посетителей.
  • Установлен WooCommerce, плагин членства или что-то на Action Scheduler - их очереди быстро отстают при запуске от загрузок страниц.

Замена на настоящий cron#

Настоящий триггер - это всё, что работает по часам и умеет либо сделать HTTP-запрос к wp-cron.php, либо запустить PHP в каталоге WordPress. Пятипольный синтаксис cron везде одинаков; если */5 * * * * нужно расшифровать, в статье выражения cron с пояснениями он разобран по полям.

Вариант 1: запрашивать wp-cron.php по HTTP

Это работает откуда угодно - с того же сервера, с другого сервера, с небольшого VDS, который у вас уже есть, или из облачного cron-сервиса, - потому что это обычный веб-запрос. Это единственный вариант, когда хостинг не даёт shell.

crontab -e
*/5 * * * * curl -fsS -o /dev/null --max-time 120 "https://example.com/wp-cron.php?doing_wp_cron"

Или через wget, который на некоторых минимальных системах есть вместо него:

bash
*/5 * * * * wget -q -O /dev/null --timeout=120 "https://example.com/wp-cron.php?doing_wp_cron"

--max-time важен. Без него зависший запрос может висеть сколько угодно, пока cron через пять минут запускает следующий. Задачи всё равно выполняются внутри обычного веб-запроса, поэтому подчиняются лимитам времени веб-сервера и самого PHP - плагину резервного копирования, которому нужно десять минут, может помешать max_execution_time или тайм-аут прокси. Какой лимит за что отвечает, разобрано в статье настройки PHP ini, которые имеют значение.

Вариант 2: запускать через WP-CLI

Если у вас есть shell на машине, где живёт WordPress, WP-CLI - триггер получше. Он работает через консольный PHP, у которого обычно нет лимита времени выполнения, ничего не проходит через веб-сервер, и нет loopback, который мог бы отказать.

crontab -e (as the site's user)
*/5 * * * * cd /var/www/example.com && /usr/local/bin/wp cron event run --due-now --quiet

Запускайте его от того же пользователя, которому принадлежат файлы WordPress, а не от root. WP-CLI неспроста отказывается работать от root без --allow-root: файлы, которые создаст задача (кэш, загрузки, архивы резервных копий), будут принадлежать root, и веб-сервер больше не сможет их менять. Если один запуск может длиться дольше интервала, оберните команду в flock -n /tmp/wp-cron-example.lock, чтобы вторая копия не стартовала поверх первой.

Прямой вызов php wp-cron.php из каталога WordPress тоже работает и не требует WP-CLI, но WP-CLI даёт гораздо лучшие инструменты, чтобы увидеть, что произошло.

Как часто запускать

СайтИнтервалПочему
Блог или сайт-визитка*/15 * * * *Ничего срочного; записи опоздают максимум на 15 минут
Блог с отложенными записями или рассылками*/5 * * * *Обычное значение по умолчанию
WooCommerce или членство* * * * *Очереди Action Scheduler и повторы платежей
Сеть Multisite*/5 * * * * на каждый сайтУ каждого сайта своя опция cron

Запуск каждую минуту дёшев, когда ничего не просрочено - запрос сверяет метку времени и выходит. Стоимость появляется, только когда накопилось много работы, а тогда вы и так хотите, чтобы она выполнялась без задержек. В Multisite помните, что у каждого сайта своя очередь: пройдите циклом по wp site list --field=url и выполните wp cron event run --due-now --url=<site> для каждого или запрашивайте wp-cron.php каждого сайта.

WP-CLI: смотрим и запускаем очередь#

WP-CLI - самый быстрый способ ответить на вопрос «работает ли cron». Первая команда показывает всё, что ждёт выполнения:

bash
$ wp cron event list --fields=hook,next_run_relative,recurrence+------------------------------------+------------------------+---------------+| hook                               | next_run_relative      | recurrence    |+------------------------------------+------------------------+---------------+| publish_future_post                | 3 hours 20 minutes ago | Non-repeating || wp_privacy_delete_old_export_files | 2 hours 1 minute ago   | 1 hour        || wp_version_check                   | 5 hours 12 minutes     | 12 hours      || wp_scheduled_delete                | 17 hours 4 minutes     | 1 day         |+------------------------------------+------------------------+---------------+

Всё, где стоит «ago», просрочено. Одно-два события с опозданием примерно на минуту - норма; целый столбец событий, ушедших в прошлое на часы, означает, что cron вообще никто не запускает. Полезные команды:

КомандаЧто делает
wp cron event listВсе запланированные события и их следующий запуск
wp cron event run --due-nowВыполняет все события, время которых прошло
wp cron event run <hook>Выполняет один хук сейчас, независимо от расписания
wp cron event schedule <hook> now hourlyДобавляет событие
wp cron event delete <hook>Удаляет запланированные события хука
wp cron schedule listПоказывает доступные интервалы
wp cron testПроверяет, может ли WP-Cron запустить loopback

Запуск одного хука через wp cron event run <hook> - лучший способ отладить задачу плагина: ошибки и предупреждения видны прямо в терминале, а не теряются в фоновом запросе, за которым никто не следит. Добавьте --debug, чтобы увидеть собственную трассировку WP-CLI о том, что загрузилось.

wp cron test проверяет путь через загрузку страниц - он отправляет тот же loopback, что и spawn_cron(), а заодно сообщает, если установлен DISABLE_WP_CRON. С включённой константой предупреждение там ожидаемо; важно тогда, разбирает ли список ваш системный cron.

Если shell нет, плагин WP Crontrol показывает тот же список прямо в админке, позволяет выполнить событие немедленно и отмечает события, за хуком которых больше нет кода. Его стоит поставить ради диагностики и удалить после.

Пропущенные запланированные записи#

«Missed schedule» в списке записей означает, что событие publish_future_post для этой записи наступило, но не выполнилось. WordPress показывает эту пометку, когда запланированное время прошло, а запись всё ещё в статусе future. Почти всегда причина одна из этих:

  1. Нет триггера. Между запланированным временем и текущим моментом никто не заходил, всё отдавалось из кэша, или установлен DISABLE_WP_CRON без замены. Событие по-прежнему в очереди; следующий запуск cron опубликует запись.
  2. Loopback не проходит. Сервер не может достучаться до собственного домена. Частые причины: DNS сайта указывает на CDN или прокси, недоступные изнутри; hairpin NAT, который отбрасывает соединения сервера с его же публичным адресом; HTTP basic auth на тестовом сайте; плагин безопасности или файрвол, блокирующие запросы к wp-cron.php; самоподписанный сертификат на локальной копии.
  3. Задача упала. Более ранняя задача в том же запуске получила фатальную ошибку или исчерпала память, и остаток очереди так и не выполнился. Один сломанный хук плагина может задержать всё, что за ним.
  4. Событие исчезло. Запись запланирована, но wp cron event list не показывает для неё publish_future_post. Это реже - обычно находят гонку, когда два запроса одновременно пишут опцию cron, или постоянный объектный кэш, отдающий устаревшую копию опций.

В первых трёх случаях исправленный триггер исправляет и запись: выполните wp cron event run --due-now, и она опубликуется. В четвёртом откройте запись и опубликуйте её вручную или задайте новое время и запланируйте снова - это создаст свежее событие. Существуют плагины, которые «публикуют пропущенные запланированные записи», и всё, что они делают, - ищут просроченные будущие записи на загрузках страниц и публикуют их. Это обход проблемы с триггером, а не её решение, и с настоящим cron он не нужен.

ALTERNATE_WP_CRON и другие обходные пути#

wp-config.php
define( 'ALTERNATE_WP_CRON', true );

ALTERNATE_WP_CRON существует для серверов, где loopback работать не может, а добавить настоящий cron нельзя. Вместо того чтобы WordPress обращался сам к себе, он перенаправляет браузер посетителя на тот же URL с добавленным ?doing_wp_cron= и выполняет просроченные задачи в этом запросе после отправки страницы. Задачи действительно начинают выполняться, но ценой: видимая строка запроса в адресной строке, лишний редирект на том просмотре страницы, который его вызвал, вариант URL, который может разойтись по ссылкам или попасть в кэш, и работа cron по-прежнему привязана к трафику. Считайте это временной мерой. Если вы можете хоть откуда-нибудь запланировать HTTP-запрос к wp-cron.php, делайте так, а эту константу не включайте.

Стоит знать ещё две связанные константы. WP_CRON_LOCK_TIMEOUT задаёт окно блокировки в секундах; для большинства сайтов увеличивать его бесполезно, но некоторые уменьшают его, когда cron на загрузках страниц оставлен и задачи должны стартовать чаще. DISABLE_WP_CRON и ALTERNATE_WP_CRON не стоит включать одновременно - при включённой первой у второй просто не будет шанса сработать.

wp-cron.php, кэширование и безопасность#

Даже с установленным DISABLE_WP_CRON файл wp-cron.php отвечает любому, кто его запросит, и выполняет всё просроченное. Обычно это безвредно - если ничего не просрочено, запрос быстро завершается, - но атакующий или сломанный бот, долбящий его в цикле, заставляет PHP загружать WordPress на каждом запросе. В статье усиление безопасности WordPress он рассматривается рядом с xmlrpc.php как точка входа, которую стоит ограничить. Если ваш cron работает на той же машине через WP-CLI, обращаться к wp-cron.php по HTTP вообще никому не нужно, и его можно закрыть на уровне веб-сервера:

nginx
location = /wp-cron.php {    allow 127.0.0.1;    deny all;}

Если его вызывает внешняя машина по HTTP, разрешите её адрес вместо 127.0.0.1 или ограничьте частоту запросов к этому location, а не блокируйте полностью. Не закрывайте его, пока замена триггера не заработала, иначе вы вернётесь к пропущенным публикациям.

Когда cron работает по часам, кэширование играет вам на руку. Полностраничный кэш перед WordPress, описанный в статье скорость и кэширование WordPress, означает, что до PHP доходит меньше запросов, а при cron на загрузках страниц это означало меньше шансов выполнить задачи. С настоящим триггером они перестают мешать друг другу: кэш обслуживает посетителей, а cron - очередь. Исключите wp-cron.php из всех правил кэширования - в большинстве конфигураций это так и есть, потому что у запроса есть строка параметров, но правило CDN «кэшировать всё» может его проглотить.

Важна и сторона памяти. Запросы cron работают под WP_MAX_MEMORY_LIMIT (по умолчанию 256M), а не под более низким лимитом фронтенда, поэтому тяжёлая задача может ненадолго занять больше, чем любой просмотр страницы. Если сайт падает каждую ночь примерно в одно и то же время, посмотрите, что cron выполняет в этот час, прежде чем винить трафик - в статье сколько RAM нужно WordPress показано, как это измерить.

Cron WordPress на RE:NODE#

Линейка WordPress в веб-хостинге RE:NODE устанавливает WordPress при первом запуске с уже написанным wp-config.php для собственной базы MySQL, так что DISABLE_WP_CRON добавляется в файл, который можно сразу открыть во встроенном файловом менеджере или по SFTP. Изначально ничего не меняется: WP-Cron работает на загрузках страниц точно так же, как везде.

В панели есть вкладка Schedules, которая принимает стандартное выражение cron и выполняет упорядоченные задачи с задержками между ними - консольную команду, резервное копирование или действие с питанием (подробности в руководстве по Schedules). Для сайта на WordPress это естественное место для задач вокруг WordPress, а не внутри него: ночной backup в час, когда никто из редакторов не работает, и backup за несколько минут до запланированной массовой правки. Как универсальный способ запросить URL она не описана, поэтому для самого триггера wp-cron.php используйте HTTP-cron снаружи - другой ваш сервер или любой облачный cron-сервис - со строкой curl, приведённой выше, направленной на ваш домен. Сайт и так отдаёт HTTPS на собственном домене через прокси-слот тарифа, так что запрос самый обычный.

Для задач cron важны две особенности платформы. CPU жёстко ограничен долей, входящей в тариф, так что тяжёлая задача на маленьком тарифе выполняется медленно, но не падает; а если задача выводит сервер за лимит памяти, контейнер останавливается и чисто перезапускается вместо ухода в swap, и этот запуск cron обрывается на полпути. Если ночная задача достаточно велика, чтобы упереться в одно из этого, разнесите её по времени или перейдите на тариф выше - общий подход описан в статье фоновые задачи на небольшом сервере.

Чек-лист для поиска неисправностей#

В `wp cron event list` события просрочены на часы. Cron никто не запускает. Если установлен DISABLE_WP_CRON, ваш системный cron не работает или не достаёт до сайта - выполните ту же строку curl вручную и посмотрите на код ответа. Если не установлен, у сайта слишком мало некэшированного трафика или не проходит loopback; wp cron test покажет, что именно.

Строка curl возвращает 403 или страницу входа. Запрос блокирует плагин безопасности, WAF или basic auth. Разрешите адрес вызывающей машины или перейдите на триггер через WP-CLI на той же машине.

Строка curl возвращает 200, но ничего не выполняется. Проверьте, что URL - настоящий адрес сайта, а не редирект на www или HTTPS: добавьте -L, чтобы следовать за ним, а лучше сразу используйте конечный URL. В Multisite проверьте, что вызываете нужный сайт.

Одна задача никогда не завершается, и всё за ней опаздывает. Запустите этот хук отдельно через wp cron event run <hook> и прочитайте ошибку. Фатальные ошибки в фоновых запросах попадают в журнал ошибок PHP, а не на экран, поэтому на время поиска включите WP_DEBUG_LOG и потом выключите.

Одна и та же задача выполняется дважды. Активны два триггера - cron на загрузках страниц и системный cron или две строки crontab, оставшиеся от старой настройки. Установите DISABLE_WP_CRON и оставьте ровно одно расписание.

Сотни событий от плагина, который вы удалили. Плагин оставил свои хуки. Удалите их через wp cron event delete <hook> или плагином WP Crontrol. Вреда от них нет, но они засоряют список и мешают заметить настоящие проблемы.

FAQ#

Останавливает ли DISABLE_WP_CRON публикацию запланированных записей?

Только если cron больше ничто не запускает. Константа убирает триггер на загрузках страниц; запланированные события остаются в очереди и выполняются, как только что-то вызовет wp-cron.php или их запустит WP-CLI. В паре с настоящим cron запланированные записи выходят точнее, а не наоборот.

Как часто настоящий cron должен вызывать wp-cron.php?

Большинству сайтов подходит раз в пять минут. Магазинам и всему, что использует Action Scheduler, лучше раз в минуту, а сайт без срочных задач может обойтись пятнадцатью. Запуск, когда ничего не просрочено, стоит одного короткого PHP-запроса.

Можно ли запускать WP-Cron без доступа к shell?

Да. Триггеру нужно лишь сделать HTTPS-запрос к вашему сайту, так что он может жить где угодно: в облачном cron-сервисе, на уже имеющемся VPS или на домашнем сервере друга. Используйте строку curl с тайм-аутом и направьте её на конечный URL сайта.

Безопасно ли использовать ALTERNATE_WP_CRON?

Работает и не опасно, но показывает посетителям строку запроса, добавляет редирект и по-прежнему зависит от трафика. Используйте его, только если loopback не проходит и нет никакой возможности запланировать внешний запрос.

Почему Site Health пишет, что запланированное событие опаздывает?

Site Health сравнивает время каждого события с текущим и отмечает просроченные. Одно опоздавшее событие обычно означает медленный триггер; целый список - что cron не работает. Проверьте через wp cron event list и чините триггер, а не предупреждение.

Ломает ли страничный кэш WP-Cron?

Ослабляет. Ответы из кэша до PHP не доходят и cron не запускают. На хорошо закэшированном сайте с cron на загрузках страниц задачи могут долго ждать некэшированного запроса. Переход на настоящий cron решает это полностью.

Это было полезно?