Cron на хостингу запускає команди і скрипти за розкладом від імені вашого користувача: задачу додають у панелі керування (зазвичай це розділ cron) або командою crontab -e через SSH. Коли задача «не працює», причина рідко в самому розкладі. Зазвичай заважає інше оточення: у cron немає вашого PATH, робочий каталог інший, знак % обрізає команду, а вивід іде листами. Нижче вісім таких помилок, які ми відтворили на тестовому стенді, з реальним виводом і виправленнями, а також вимірювання WP-Cron проти системного cron.
Як ми перевіряли
10 листопада 2026 року на тестовому стенді: контейнер Ubuntu 24.04.5, пакет cron 3.0pl1-184ubuntu2 (стандартний cron Debian і Ubuntu, на яких зазвичай працює панель керування власної розробки з ШІ-помічником), окремий користувач без прав root, PHP 8.3.6 у двох режимах (CLI і PHP-FPM з memory_limit 128M), nginx 1.24, MariaDB 10.11, WordPress 6.4.3. Кожну задачу ставили на запуск щохвилини і перевіряли журнал cron, листи, які він надсилає, та власні журнали задач. Таймери systemd перевірили лише утилітою systemd-analyze, бо в контейнері systemd не запущений. Шляхи в прикладах належать стенду. Це поведінка тестового стенда, а не дані із серверів RichHost.
Як читати рядок crontab
Рядок складається з п’яти полів часу і команди. Час рахується за годинником сервера, а не за вашим (чому це важливо, видно в помилці №6).
# хв год день міс день_тижня команда */5 * * * * команда # кожні 5 хвилин 15 3 * * * команда # щодня о 03:15 0 9 * * 1-5 команда # о 09:00 у будні 30 2 1 * * команда # 1 числа щомісяця о 02:30 @daily команда # раз на добу о 00:00
Одна пастка є вже в синтаксисі: якщо заповнити і день місяця, і день тижня (0 3 1 * 1), задача спрацює і першого числа, і щопонеділка, а не лише в понеділок, що випав на перше.
Як додати задачу на хостингу
У панелі керування
У панелі керування хостингом відкрийте розділ cron: там зазвичай є кнопка додавання, поле для команди і п’ять полів розкладу. Задача виконується від імені вашого користувача, тому sudo в ній недоступне. Пишіть повний шлях до інтерпретатора, наприклад /usr/bin/php8.2: версія PHP для cron може відрізнятися від тієї, що вибрана для сайту, тож спершу дізнайтеся, яку версію PHP використовує сайт. На тарифах RichHost сайтами керують через власну панель керування з ШІ-помічником, а SSH-доступ є на всіх тарифах.
Через SSH
Якщо ви вже вмієте підключитися по SSH, розклад редагують командою crontab -e, переглядають через crontab -l. Обережно з crontab -r: він без запитань видаляє весь розклад. Надійний рядок задачі виглядає так:
MAILTO="" */10 * * * * cd /home/user/web/example.com/public_html && flock -n /home/user/tmp/export.lock /usr/bin/php8.3 cron/export.php >> /home/user/logs/export.log 2>&1
Тут закрито одразу п’ять пасток із наступного розділу: робочий каталог, повний шлях до PHP, захист від накладання, журнал замість листів і жодного знака % у команді.
Вісім помилок, які ми відтворили
1. У SSH команда працює, а в cron пише «not found»
Cron дає задачі мінімальне оточення. Ось що задача записала у файл командою env:
HOME=/srv/cw6/home/cw6user LOGNAME=cw6user PATH=/usr/bin:/bin SHELL=/bin/sh PWD=/srv/cw6/home/cw6user
У сесії SSH того самого користувача composer лежав у /usr/local/bin, якого в цьому PATH немає. Результат у журналі задачі і код виходу 127 у журналі cron:
/bin/sh: 1: composer: not found (CRON) error (grandchild #19489 failed with exit status 127)
Виправлення: повний шлях (/usr/local/bin/composer, дізнатися його можна командою which composer у SSH) або рядок PATH=... на початку crontab. Це стосується також node, wp та конкретних версій php. Зверніть увагу і на SHELL=/bin/sh: синтаксис bash на кшталт [[ ]] або source краще винести в окремий скрипт із першим рядком #!/bin/bash.
2. Відносні шляхи: скрипт не бачить свого конфігу
Скрипт із рядком require './config.php'; чудово працював із каталогу сайту. У cron робочим каталогом є домашній каталог, тому той самий скрипт падав щохвилини:
PHP Warning: require(./config.php): Failed to open stream: No such file or directory in .../site/task.php on line 2 PHP Fatal error: Uncaught Error: Failed opening required './config.php' (include_path='.:/usr/share/php')
Виправлення: cd /шлях/до/сайту && php task.php (на стенді після цього задача відпрацьовувала щохвилини) або шляхи в самому скрипті від __DIR__.
3. Знак % обрізає команду
У crontab усе після першого неекранованого % передається команді як стандартний ввід. Рядок echo "run $(date +%Y-%m-%d)" >> e3.log 2>&1 не створив журналу взагалі, бо перенаправлення опинилося після %, а користувач отримав лист:
Subject: Cron <cw6user@vm> echo "run $(date + (failed) /bin/sh: 1: Syntax error: end of file unexpected (expecting ")")
Виправлення: date +\%Y-\%m-\%d або перенесення команди в скрипт, де % екранувати не треба.
4. Задачу мовчки проігноровано через формат файлу
Команда crontab перевіряє файл перед встановленням і не прийняла файл без символу нового рядка в кінці (new crontab file is missing newline before EOF, can't install.), а також порожню змінну MAILTO= без лапок (bad minute; правильно MAILTO=""). Набагато підступніше на VPS, де файли кладуть просто в /etc/cron.d. На стенді з чотирьох файлів спрацював лише один:
- без символу нового рядка в кінці:
ERROR (Missing newline before EOF, this crontab file will be ignored); - з крапкою в імені (
backup.cron): проігноровано без жодного запису в журналі; - з правами 664:
INSECURE MODE (group/other writable)щохвилини, задача не запускалась; backup-okз правами 644 і символом нового рядка: працював.
5. Лист на кожен запуск замість журналу
Якщо команда щось виводить і вивід нікуди не перенаправлено, cron надсилає його листом власнику. Дві такі задачі щохвилини дали два листи на хвилину, тобто 120 на годину, з темою на кшталт Cron <cw6user@vm> php -r 'echo "processed 42 orders\n";'. Коли поштового сервера немає, cron пише No MTA installed, discarding output і вивід просто зникає, разом із повідомленнями про помилки.
Виправлення: >> /шлях/job.log 2>&1 для журналу або MAILTO="", якщо вивід не потрібен. Компромісний варіант: >> job.log без 2>&1, тоді звичайний вивід іде в журнал, а лист приходить лише тоді, коли скрипт пише помилку.
6. Часовий пояс: CRON_TZ на Ubuntu не працює
Сервер рахує розклад у своєму часовому поясі, часто це UTC. Ми поставили CRON_TZ=Asia/Kathmandu і час 19:31 за Катманду (13:46 UTC): задача не запустилася ні о 13:46, ні пізніше. Довідка crontab(5) цього пакета прямо каже, що окремі часові пояси для користувачів не підтримуються. Змінна TZ=Europe/Kyiv у crontab змінює тільки вивід date усередині задачі (date says 15:44:01 EET, utc 13:44:01), а не момент запуску. Ще одна знахідка: стара назва Europe/Kiev на Ubuntu 24.04 без пакета tzdata-legacy мовчки дає UTC.
Виправлення: перерахуйте час у пояс сервера (03:15 за Києвом взимку це 01:15 UTC, влітку 00:15) або запускайте задачу щогодини і перевіряйте годину в самій команді: [ "$(TZ=Europe/Kyiv date +\%H)" = 03 ] && .... У cronie (AlmaLinux, Rocky Linux) CRON_TZ підтримується, у таймерах systemd пояс пишуть прямо в розкладі.
7. Наступний запуск стартує, поки попередній ще працює
Скрипт, що працює 150 секунд, ми запускали щохвилини. О 13:46 у системі одночасно жили три його копії (13:44, 13:45 і 13:46). Для резервного копіювання чи імпорту товарів це означає подвійні записи і конкуренцію за диск. З flock -n друга копія просто не стартує:
13:50:01 start pid=22979 13:51:02 skipped: previous run still holds the lock 13:52:01 skipped: previous run still holds the lock 13:52:31 end pid=22979 13:53:01 start pid=3603
Пастка, на яку ми натрапили самі: файл блокування лежав у каталозі, куди користувач не мав права писати. flock завершувався з кодом 66 (cannot open lock file ... Permission denied), а конструкція || echo skipped щохвилини писала «skipped», хоча скрипт не запускався жодного разу. Тримайте файл блокування у своєму домашньому каталозі.
8. PHP у cron не той, що на сайті
Cron запускає PHP у режимі CLI, а сайт обслуговує PHP-FPM. Це різні файли налаштувань і різні обмеження:
| Параметр | Cron (php-cli) | Сайт (PHP-FPM) |
|---|---|---|
| php.ini | /etc/php/8.3/cli/php.ini | /etc/php/8.3/fpm/php.ini |
| memory_limit | -1 (без обмеження) | 128M |
| max_execution_time | 0 (без обмеження) | 30 секунд |
Скрипт, що займає 200 МБ, у cron відпрацював (allocated 200 MB under cli), а той самий файл, відкритий за URL, дав HTTP 500 і Allowed memory size of 134217728 bytes exhausted. Тому задача, яку викликають через wget за адресою, живе за правилами сайту, а не cron. На спільному хостингу важкі задачі впираються ще й у ліміти акаунта, а їхнє перевищення може закінчитися помилкою 508.
WP-Cron проти системного cron: що показав стенд
WP-Cron не є окремою службою. Під час завантаження сторінки WordPress перевіряє список задач і, якщо щось уже пора виконати, надсилає сам собі фоновий запит до wp-cron.php. На стенді ми створили подію кожні 5 хвилин і запланований запис, а потім не заходили на сайт:
- подія, запланована на 13:44:32, виконалась о 13:46:27, коли сторінку завантажили вперше (на 115 секунд пізніше);
- за 8,5 хвилини без відвідувачів запланований запис так і лишився в статусі «заплановано», а наступна подія чекала;
- перший запит о 13:55:10 запустив
wp-cron.php: подію виконано із запізненням 338 секунд, запис опубліковано через 518 секунд після запланованого часу, а пропущений запуск о 13:54:32 окремо не повторювався; - самого відвідувача фоновий запуск не сповільнив: TTFB 0,074 с проти 0,066-0,079 с без нього, бо
wp-cron.phpодразу віддає відповідь і працює далі в окремому процесі PHP.
Потім вимкнули WP-Cron і поставили системний cron кожні 5 хвилин:
// wp-config.php, вище рядка "That's all, stop editing!" define( 'DISABLE_WP_CRON', true );
*/5 * * * * cd /home/user/web/example.com/public_html && /usr/bin/php wp-cron.php >/dev/null 2>&1
Після цього три відвідування поспіль уже не викликали wp-cron.php, а новий запланований запис вийшов о 14:00:02, на найближчому запуску cron, через 211 секунд після свого часу. Тепер запізнення не залежить від відвідувачів і не перевищує інтервалу cron. Про інші налаштування швидкості йдеться в матеріалі про те, як прискорити WordPress.
Варіант через адресу (wget -q -O /dev/null "https://example.com/wp-cron.php?doing_wp_cron") теж робочий, але виконується за обмеженнями PHP-FPM із пункту 8 і може натрапити на захист від ботів. Якщо файли сайту в тому самому акаунті, надійніший виклик через php.
Таймери systemd на власному VPS
На спільному хостингу таймерів systemd немає, бо для них потрібен root. На власному сервері вони зручніші за cron у трьох речах: Persistent=true запускає задачу, пропущену під час вимкнення сервера, часовий пояс пишуть прямо в розкладі, а вивід потрапляє в журнал, а не в пошту.
# /etc/systemd/system/site-backup.service [Unit] Description=Nightly site backup [Service] Type=oneshot User=user WorkingDirectory=/home/user/site ExecStart=/usr/bin/php /home/user/site/cron/backup.php # /etc/systemd/system/site-backup.timer [Unit] Description=Run site-backup daily at 03:15 Kyiv time [Timer] OnCalendar=*-*-* 03:15:00 Europe/Kyiv Persistent=true RandomizedDelaySec=300 [Install] WantedBy=timers.target
Перед увімкненням перевірте розклад: systemd-analyze calendar "*-*-* 03:15:00 Europe/Kyiv" на стенді показав наступний запуск о 01:15 UTC, а systemd-analyze verify на помилковому 25:15:00 відповів Failed to parse calendar specification і Timer unit lacks value setting. Refusing. Далі systemctl enable --now site-backup.timer, список таймерів у systemctl list-timers, вивід у journalctl -u site-backup.service. Як підготувати сам сервер, описано в чеклісті налаштування VPS на Ubuntu 24.04.
Симптом, причина, виправлення
| Симптом | Причина | Виправлення |
|---|---|---|
| «not found», код виходу 127 | У cron PATH лише /usr/bin:/bin | Повний шлях до програми або PATH= у crontab |
| «Failed to open stream», хоча в SSH усе працює | Робочий каталог cron це домашній каталог | cd /шлях && перед командою або __DIR__ |
| Журнал не створюється, лист «Syntax error» | Неекранований % у команді | \% або окремий скрипт |
| Задача з /etc/cron.d не запускається | Немає символу нового рядка, крапка в імені, права 664 | Ім’я без крапки, права 644, новий рядок у кінці |
| Скринька забита листами від Cron Daemon | Вивід не перенаправлено | >> job.log 2>&1 або MAILTO="" |
| Задача спрацьовує на 2-3 години раніше чи пізніше | Сервер у UTC, CRON_TZ ігнорується | Перерахувати час або перевіряти годину в команді |
| Подвійні записи, кілька копій у списку процесів | Новий запуск до завершення попереднього | flock -n з файлом блокування у своєму каталозі |
| У cron працює, за URL помилка 500 | Різні php.ini для CLI і FPM | Викликати через php, підняти ліміт для конкретного скрипта |
| Позначка Missed schedule у WordPress | WP-Cron чекає на відвідувача | DISABLE_WP_CRON і системний cron кожні 5 хвилин |
Часті питання
Як часто можна запускати cron?
Найменший крок у crontab одна хвилина. Для WordPress і більшості сайтів досить 5-15 хвилин, для резервних копій і звітів раз на добу вночі. Частіший запуск має сенс лише для черг, і тоді обов’язково з flock.
Як переконатися, що задача справді виконується?
Додайте в команду запис часу в журнал, наприклад date >> job.log перед основною командою, і через кілька хвилин подивіться файл. Порожній журнал означає, що задача не стартувала, а рядок з помилкою підкаже, яка з восьми пасток спрацювала.
Що краще: php script.php чи wget за адресою?
Якщо скрипт лежить у вашому акаунті, кращий виклик через php: немає обмеження 30 секунд, запит не проходить через вебсервер і захист від ботів. Адреса потрібна, коли скрипт на іншому сервері або плагін вимагає саме HTTP-запиту.
Чи виконає cron задачу, пропущену під час перезавантаження сервера?
Ні. Cron не наздоганяє пропущені запуски: якщо в потрібну хвилину служба не працювала, задача чекатиме наступного разу за розкладом. У таймерах systemd для цього є Persistent=true.



