Сообщения service holdoff time over и restart counter is at в Debian/Ubuntu почти всегда означают одно: systemd пытается автоматически поднять сервис после сбоя, но процесс снова завершается с ошибкой. В итоге получается типичный restart loop — цикл перезапусков, который либо продолжается, либо уже остановлен защитными лимитами менеджера сервисов.
На практике проблема редко бывает в самом systemd. Обычно это только симптом. Истинная причина почти всегда ниже уровнем: неверный путь к бинарнику, ошибка в конфиге, отсутствующий каталог, неправильный пользователь, занятый порт, недоступная база данных, неверные переменные окружения или слишком агрессивные параметры рестарта.
Частая ошибка администраторов — сразу править Restart=always, RestartSec или увеличивать лимиты, не выяснив, почему процесс вообще падает. Это маскирует симптом, но не лечит сервис.
Ниже разберём, что означают строки holdoff time over и restart counter is at, как правильно читать вывод systemctl status, какие команды запускать через journalctl, как отделить проблему unit-файла от ошибки приложения и когда действительно имеет смысл менять Restart, RestartSec, StartLimitBurst и StartLimitIntervalSec.
Материал особенно полезен для сервисов собственного развёртывания: Node.js, Python, Go, Java, очередей, API-демонов и любых процессов, которые запускаются через unit-файл в /etc/systemd/system. Если вы поднимаете такие приложения на VDS, этот сценарий встретится рано или поздно почти наверняка.
Что означают сообщения holdoff time over и restart counter is at
Когда systemd запускает сервис с политикой автоматического перезапуска, он отслеживает число неудачных стартов и выдерживает паузу между попытками. Эта пауза задаётся параметром RestartSec. После неё в журнале и появляется строка вида Service hold-off time over, scheduling restart. Это не отдельная ошибка, а уведомление: время ожидания истекло, менеджер собирается снова стартовать юнит.
Сообщение restart counter is at N означает, что systemd уже несколько раз пытался перезапустить сервис и ведёт счётчик таких попыток. Если этот счётчик превысит разрешённый порог в заданный интервал, сработает защита от бесконечного флаппинга, и вы увидите уже другой итоговый симптом — отказ в запуске из-за превышения лимита стартов.
holdoff time over— это следствие политики рестарта, аrestart counter is at— индикатор того, что сервис вошёл в повторяющийся цикл падений.
Поэтому главный вопрос не в том, как убрать эти строки из журнала, а в том, что именно заставляет процесс завершаться. Если приложение стартует и сразу выходит с кодом 1, systemd будет послушно перезапускать его, пока не упрётся в лимиты.
С чего начинать диагностику
Первое правило: не смотреть только на состояние «active/inactive». Важны последние строки статуса, код выхода, причина завершения и журнал юнита.
Начните с базовой проверки:
systemctl status myservice.service --no-pager -l
systemctl show myservice.service -p ExecMainStatus -p ExecMainCode -p Result -p Restart -p RestartUSec
journalctl -u myservice.service -b --no-pager -n 100
Что искать в выводе:
- строку
Main PIDи чем завершился процесс; status=1/FAILURE,status=203/EXEC,status=217/USERи другие коды;- сообщения приложения прямо перед завершением;
- повторяющиеся рестарты с одинаковым интервалом;
- упоминание превышения лимита стартов.
Очень полезно смотреть журнал только за текущую загрузку системы. Ключ -b отсекает старый шум и показывает актуальную картину. Если сервис уже давно флапает, сузьте интервал:
journalctl -u myservice.service --since "10 minutes ago" --no-pager
journalctl -xeu myservice.service --no-pager
Команда systemctl status хороша для краткой сводки, но почти всегда недостаточна. Основная фактура лежит в journalctl.

Типичный сценарий: systemd исправен, сломан сам процесс
В большинстве случаев юнит написан формально правильно, а сервис падает из-за своей внутренней ошибки. Например, приложение ждёт файл конфигурации в /opt/app/config.yml, а файл отсутствует. Или пытается слушать порт, который уже занят другим процессом. Или стартует от пользователя, у которого нет доступа к рабочему каталогу.
Вот самые частые реальные причины:
- неверный путь в
ExecStart; - скрипт не имеет права на исполнение;
- не существует пользователя из параметра
User; - не существует каталога из
WorkingDirectory; - приложение пишет PID, сокет или логи туда, где нет прав;
- процесс требует переменные окружения, которых нет в systemd-окружении;
- конфиг приложения содержит синтаксическую ошибку;
- порт уже занят, сокет не создаётся;
- сервис зависит от сети, DNS, базы данных или другого юнита, который ещё не готов;
- неправильно выбран тип юнита: например, указан
Type=forkingдля процесса, который не форкается.
Если в журнале нет ясной причины, попробуйте запустить команду из ExecStart вручную от того же пользователя и из того же каталога. Это один из самых быстрых способов вскрыть расхождение между «работает в shell» и «не работает в systemd».
systemctl cat myservice.service
id appuser
sudo -u appuser sh -lc 'cd /opt/myapp && /opt/myapp/bin/start-app'
Если вручную команда сразу показывает ошибку, вы нашли корень проблемы. Если вручную всё работает, а через systemd нет — проверяйте окружение, Type, зависимости и ограничения юнита.
Как читать коды ошибок systemd
Некоторые коды выхода особенно полезны при разборе циклов рестарта. Они помогают понять, ошибка возникла ещё до запуска приложения или уже внутри него.
status=203/EXEC
Обычно это значит, что systemd не смог выполнить команду из ExecStart. Причины типовые: неверный путь, отсутствие файла, нет бита исполнения, плохой shebang в скрипте, неподходящая архитектура бинарника.
ls -l /opt/myapp/bin/start-app
file /opt/myapp/bin/start-app
head -n 1 /opt/myapp/bin/start-app
status=217/USER
Чаще всего это проблема с параметром User или Group. Пользователь не существует, NSS не готов, либо systemd не может переключить контекст запуска.
getent passwd appuser
getent group appgroup
status=1/FAILURE или другой код приложения
Здесь systemd обычно ни при чём: бинарник запустился и сам завершился с ошибкой. Значит, нужен лог приложения, проверка конфигурации и ручной запуск.
Result=start-limit-hit
Это уже следствие, а не причина. Сервис падал слишком часто, и systemd прекратил попытки запуска до ручного вмешательства или до истечения лимитного окна.
Параметры Restart, RestartSec, StartLimitBurst и StartLimitIntervalSec
Именно эти параметры чаще всего всплывают рядом с ошибками holdoff time over и restart counter is at. Важно понимать их смысл по отдельности.
Restart=
Параметр определяет, когда сервис надо перезапускать.
no— не перезапускать;on-failure— перезапускать при ошибке, ненулевом коде или аварийном завершении;always— перезапускать почти всегда, даже после «успешного» выхода процесса.
Для большинства демонов и фоновых приложений разумнее начинать с Restart=on-failure. Параметр Restart=always полезен не всегда: если приложение по дизайну должно иногда завершаться штатно, вы получите ложный цикл рестартов.
RestartSec=
Это пауза между попытками перезапуска. Если поставить слишком маленькое значение, сервис будет быстро засыпать журнал повторяющимися ошибками. Если слишком большое — восстановление после кратковременного сбоя затянется.
На практике часто подходят значения от 2 до 10 секунд. Для зависимостей, которые могут подниматься дольше, иногда разумнее 15–30 секунд.
StartLimitBurst=
Задаёт, сколько неудачных стартов допустимо в пределах окна. Когда лимит исчерпан, systemd перестаёт автоматически рестартовать юнит.
StartLimitIntervalSec=
Это размер окна, в течение которого считается число неудачных запусков. Например, если задано StartLimitBurst=5 и StartLimitIntervalSec=60, то после пяти неудачных стартов за минуту сервис упрётся в лимит.
Эти параметры не лечат приложение. Они только защищают систему от бесконечного и шумного restart loop.
Пример корректировки unit-файла
Допустим, сервис стартует слишком рано и падает, если внешняя зависимость ещё не готова. В таком случае можно аккуратно скорректировать unit:
[Unit]
Description=My App
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/default/myapp
ExecStart=/opt/myapp/bin/myapp
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
После изменения unit-файла не забывайте перечитать конфигурацию менеджера и сбросить счётчик неудачных стартов, если лимит уже достигнут:
systemctl daemon-reload
systemctl reset-failed myservice.service
systemctl restart myservice.service
systemctl status myservice.service --no-pager -l
Команда reset-failed особенно важна. Иначе вы можете исправить конфиг, но юнит останется в состоянии отказа из-за ранее сработавшего лимита.
Если у вас несколько фоновых воркеров или обработчиков очередей, полезно также посмотреть, как организован запуск воркеров через Supervisor и systemd.

Когда менять лимиты запуска, а когда не надо
Очень соблазнительно просто увеличить StartLimitBurst и надеяться, что сервис «как-нибудь поднимется». Иногда это допустимо, но только если вы уверены, что причина временная: например, после загрузки система ждёт сеть, удалённое хранилище или базу данных.
Если же приложение падает из-за синтаксической ошибки в конфиге, рост лимитов ничего не даст. Вы лишь получите больше одинаковых падений в журнале.
Сначала устраняют первопричину падения процесса, и только потом подбирают политику рестарта. Не наоборот.
Изменять лимиты обычно уместно в таких случаях:
- сервис зависит от внешней БД или API, которые могут быть недоступны короткое время;
- после reboot сеть или DNS действительно поднимаются не мгновенно;
- приложению нужен больший интервал между повторными подключениями;
- вы осознанно хотите ограничить скорость флаппинга.
Не стоит менять лимиты как первое действие, если:
- в журнале уже есть чёткая ошибка запуска;
ExecStartуказывает на несуществующий файл;- у процесса нет прав на каталог или сокет;
- неверно задан пользователь;
- приложение завершается сразу после старта из-за собственного бага.
Полезный алгоритм поиска причины за 10 минут
- Посмотрите
systemctl statusи зафиксируйте код ошибки,Resultи последние строки лога. - Откройте журнал через
journalctl -u myservice.service -b -n 100. - Найдите первую ошибку приложения, а не только сообщения о рестартах.
- Проверьте unit через
systemctl catи убедитесь, что верныExecStart,User,Group,WorkingDirectory,EnvironmentFile. - Запустите команду вручную от того же пользователя.
- Проверьте зависимости: сеть, сокеты, каталоги, порты, базы данных.
- Только после этого корректируйте
Restart,RestartSec,StartLimitBurstиStartLimitIntervalSec. - Сделайте
systemctl daemon-reload, затемsystemctl reset-failedи повторный запуск.
Частые ошибки в unit-файлах, которые запускают restart loop
Неподходящий Type=
Если приложение работает в foreground, обычно нужен Type=simple или иногда Type=exec. Если указать Type=forking для обычного процесса без демонизации, systemd будет неверно интерпретировать его поведение.
Shell-конструкции прямо в ExecStart
Systemd не исполняет строку как shell-команду автоматически. Если вы вставили туда сложную shell-логику без явного вызова интерпретатора, поведение может оказаться не тем, что ожидается.
Лучше либо вызывать конкретный бинарник, либо вынести подготовительную логику в отдельный исполняемый скрипт.
Зависимость от интерактивного окружения
Команда работает у вас в SSH-сессии, потому что там есть PATH, переменные, домашний каталог и другие привычные условия. Под systemd всего этого может не быть. Явно указывайте полные пути и нужные переменные через Environment= или EnvironmentFile=.
Сервис успешно завершился, но задан Restart=always
Это часто бывает со скриптами одноразовой инициализации. Они отрабатывают корректно, выходят с кодом 0, а systemd запускает их снова и снова. Для таких задач нужен либо другой тип юнита, либо отказ от постоянного рестарта.
Как понять, что проблема в порядке запуска после reboot
Если после ручного systemctl restart сервис стартует нормально, а после перезагрузки уходит в цикл рестартов, почти наверняка проблема в зависимостях и порядке инициализации. Обычно это сеть, DNS, монтирования или база данных.
Проверьте, есть ли в unit-файле корректные зависимости:
[Unit]
After=network-online.target
Wants=network-online.target
Но не переоценивайте эти директивы. Они не гарантируют, что удалённый API, PostgreSQL, MySQL, Redis или внешний endpoint уже готовы принимать соединения. Иногда правильнее увеличить RestartSec и сделать приложение устойчивым к временной недоступности зависимостей. Для связанных тем может пригодиться статья про усиление и изоляцию systemd-сервисов на VDS.
Что делать после исправления
Когда вы нашли и устранили проблему, стоит убедиться, что сервис не только запустился один раз, но и стабильно держится в активном состоянии.
systemctl restart myservice.service
systemctl is-active myservice.service
systemctl show myservice.service -p ActiveState -p SubState -p NRestarts
journalctl -u myservice.service --since "5 minutes ago" --no-pager
Поле NRestarts удобно для быстрой проверки: если число продолжает расти, цикл рестартов не устранён до конца. Также полезно перепроверить, не остались ли старые ошибки приложения в собственном лог-файле, если оно пишет не только в journald.
Краткие выводы
Сообщения systemd service holdoff time over и restart counter is at не являются корнем проблемы. Это маркеры того, что сервис завершился неуспешно и systemd пытается восстановить его согласно политике рестарта.
Главные инструменты для разбора — systemctl status и journalctl. Они позволяют быстро отделить ошибку unit-файла от ошибки самого приложения. Параметры Restart, RestartSec, StartLimitBurst и StartLimitIntervalSec важны, но настраивать их нужно только после понимания первопричины.
Если говорить совсем практично, правильный порядок такой: посмотреть журнал, воспроизвести запуск вручную, исправить unit или приложение, сбросить счётчик через systemctl reset-failed, затем проверить стабильность. В большинстве случаев этого достаточно, чтобы убрать restart loop без шаманства и бесконечного перебора параметров.


