6.4 Restart
When a restart-eligible cause occurs, or a Simple clean exit produces
CleanExitRestart, peinit evaluates the restart policy. The outcome is
either Failed, or Backoff followed by Starting.
evaluate_restart(service, cause):
// 1. Policy.
if cause is never-restart:
return STAY_FAILED
if cause == CleanExitRestart and policy != Always:
return INVALID_CAUSE_FOR_POLICY
if policy == Never:
return STAY_FAILED
if policy == OnFailure:
// A termination counts as success only when the cause is a
// process exit whose code is in SuccessExitCodes. Only
// ProcessCrash and CleanExitRestart carry an exit code, and
// CleanExitRestart was handled above. Every other eligible
// cause has no exit code and is always a failure here.
if cause == ProcessCrash and exit_code in success_exit_codes:
return STAY_FAILED
// A clean terminal logout is not a failure.
if cause == CleanExitRestart and TTYPath is set:
consecutive_failures = 0
return RESTART_AFTER(min(restart_delay, 60))
// Other Always exits fall through.
// 2. Budget.
if consecutive_failures >= restart_max_retries:
cause = RestartBudgetExhausted
if error_control == Critical:
sync and reboot
return STAY_FAILED
// 3. Delay.
delay = min(restart_delay << consecutive_failures, 60)
// 4. Schedule.
return RESTART_AFTER(delay)
RESTART_AFTER puts the service in Backoff for the delay; when it
elapses the service transitions to Starting and the ordinary activation
sequence begins, with its own fresh StartTimeout.
The elapsed backoff timer is consumed when the relaunch is admitted. If a pre-start check needs an asynchronous filesystem helper, the service remains in Backoff until that check finishes, retaining its terminal reservation. The pending check drives progress; the expired timer cannot admit another restart while it waits.
The relaunch runs the pre-start checks like any other start, and the
machine can have changed while the service waited. If its terminal is
now held (§11.6) or a condition no longer holds, the service goes from
Backoff to Skipped, with the cause the check gives, and its pending
operation completes as a skip. A relaunch that peinit itself cannot
execute — its bookkeeping refuses the start — takes the service from
Backoff to Failed under InternalError (§6.3), fails the operation
waiting on the restart with the internal_error result, and reports
both on the console; it does not cost the machine.
The exit code is available only when peinit observed a process exit, and
it is carried into the evaluation wherever there was one — including the
pre-readiness exit, where a Simple service exits before signalling
READY=1. So SuccessExitCodes means the same thing on both sides of
readiness: a service that legitimately concludes "nothing to do" during
startup and exits with a listed code is not restarted.
Failures with no process exit behind them — a hook that never ran, a
readiness deadline, a dependency failure — carry no code, and the
SuccessExitCodes branch cannot apply to them.
That
is not a gap: a success code is a statement about how the service's own
process ended.
6.4.1 The policies #
| Policy | Value | Behaviour |
|---|---|---|
| Never | 0 | Never restart. The service stays Failed. |
| OnFailure | 1 | Restart on a non-zero exit or a runtime failure. An exit matching SuccessExitCodes is not restarted. |
| Always | 2 | Restart on any failure regardless of exit code, and for a Simple service also on a successful clean exit. |
For a Oneshot with RestartPolicy=Always, a successful exit is not
restart-eligible. RestartPolicy governs the response to failures; a
Oneshot that succeeds has done its job. It goes to Completed — and then
Inactive without RemainAfterExit — whatever the policy says, and only
a non-zero exit reaches the restart evaluation at all.
Timer triggers are the mechanism for re-running a Oneshot on a schedule.
6.4.2 Backoff #
The delay doubles on each consecutive failure, starting from
RestartDelay and capped at 60 seconds.
The arithmetic is
overflow-safe, so a large RestartDelay or a long failure run saturates
at the cap rather than wrapping.
While a service is in Backoff it is down and does not satisfy
dependents. An explicit start creates or merges into a deferred
start operation, which honours the remaining delay rather than
short-circuiting it;
a
stop cancels the pending restart and takes the service to Inactive.
For a terminal-attached service, a successful Always exit is a normal session
logout. It resets the failure counter and uses the base delay (capped at 60
seconds), without spending or exhausting the crash budget. Failed exits and
startup failures still use the ordinary budget and exponential backoff.
6.4.3 The budget, and when it resets #
consecutive_failures counts consecutive restart-eligible failures. It
resets to zero only after the service has stayed Active for
RestartWindow seconds.
It is not a
count of restarts within a trailing window, and the difference is what
the mechanism turns on.
peinit stamps the moment a service becomes dependent-satisfying, and
clears that stamp on any transition to a non-satisfying state — so a
crash restarts the clock.
The reset fires when the
stamp plus RestartWindow is reached with the service still Active.
A service that recovers and stays Active longer than RestartWindow
between crashes therefore never exhausts its budget: each crash starts
from a counter of zero.
Only failures recurring faster than the service can sustain a window of
health accumulate.
Two other events also zero the counter, both of which mean the service is no longer in a failure run: a clean exit to Inactive, and an administrative reset. An explicit stop while in Backoff does not — the accrued failures are preserved.
Because the reset requires the service to be Active, a service that happens to be Reloading when its window boundary passes misses that reset and gets it on returning to Active.
6.4.4 Exhaustion #
Once RestartMaxRetries restarts have happened without the service
sustaining a window of health, the next failure is not restarted: Failed
with cause RestartBudgetExhausted.
peinit then
applies ErrorControl:
- Normal — the service stays Failed.
- Critical — peinit syncs the filesystems and reboots immediately.
The reboot takes precedence over
OnFailure.
The reboot does not depend on how the budget was exhausted. The paths
that observe a terminal outcome for a running service — the main job
ending, a health check failing or timing out, the watchdog expiring —
raise it directly. Every other route reaches it through a check made
once per runtime turn, which asks a single question of the service
table: is a Critical service Failed with cause RestartBudgetExhausted?
That covers a budget exhausted purely by startup failures — repeated
ReadinessTimeout, PreHookFailure, ParentSetupFailure — where a
service that can never get as far as running would otherwise have
settled in Failed with neither escalation.
The OnFailure suppression asks the same question rather than
inspecting ErrorControl itself, so the handler is skipped exactly when
a reboot is owed and never on the strength of one that was not
scheduled.
A failure during a shutdown reboots nothing (§12.2 step 2), and the handler is not started either — the machine is already going down.