A service exits with status 1. systemd restarts it. It exits again. Two more restarts, then the unit goes to failed and stays there. systemctl status ends with:
Scheduled restart job, restart counter is at 3. Start request repeated too quickly. Someone runs systemctl start app and systemd refuses that too. The natural conclusion is that the restart policy is broken, or that the unit is stuck. Neither is true. Two separate mechanisms are at work, and they answer different questions:
- Restart= decides whether this particular death deserves a restart.
- Start rate limiting decides whether the unit is allowed to start again at all.
Most "systemd stopped restarting" incidents are the second mechanism doing its job while an operator is reading the first one.
Two decisions, not one
The service manager does something close to this on every exit:
if the unit was stopped by an administrator: do not restart else if the exit cause matches Restart=: schedule a restart after RestartSec= else: leave the unit inactive or failed before any start: if the unit has been started more than StartLimitBurst times within StartLimitIntervalSec: refuse the start That order matters. Restart= can be perfectly configured and still produce a unit that stops retrying, because the start limiter is a gate in front of every start attempt, not a property of the restart policy.
Restart= is a classifier, not a boolean
The value of Restart= is matched against how the process died, not against what you meant. systemd.service(5) documents the full matrix:
| Exit cause | no | always | on-success | on-failure | on-abnormal | on-abort | on-watchdog |
|---|---|---|---|---|---|---|---|
| Clean exit code or signal | X | X | |||||
| Unclean exit code | X | X | |||||
| Unclean signal | X | X | X | X | |||
| Timeout | X | X | X | ||||
| Watchdog | X | X | X | X |
A "clean" exit is not just exit code 0. For service types other than Type=oneshot, systemd also treats termination by SIGHUP, SIGINT, SIGTERM, or SIGPIPE as clean, plus anything you list in SuccessExitStatus=. That is deliberate: a daemon that exits 0 or catches SIGTERM during an ordinary shutdown should not be treated like a crash.
The rest of the table follows from that definition:
-
on-failurerestarts on nonzero exit codes, unclean signals (including core dumps), start/reload/stop timeouts, and watchdog expiry. It is the recommended default for long-running services. -
on-abnormalrestarts on signals, timeouts, and watchdog expiry, but not on a plain nonzero exit code. Use it for a daemon that is allowed to decide its own exit status without being resurrected. -
on-abortis narrower: an uncaught signal that is not in the clean-exit set. -
on-watchdogonly reacts to a missed keep-alive deadline. -
on-successis for the odd service that is supposed to run and exit cleanly, then run again.
One detail that surprises people: the process that dies does not have to be the main daemon. A failure in ExecStartPre=, ExecStartPost=, ExecStop=, ExecStopPost=, or ExecReload= can also trigger the restart logic. If your "main process" never even started, check the pre-start commands before blaming the application.
The Type=oneshot trap
For Type=oneshot, a clean exit is a completed job, so Restart=always and Restart=on-success make no sense. systemd rejects them at load time. systemd-analyze verify states it plainly:
Service has Restart= set to either always or on-success, which isn't allowed for Type=oneshot services. Refusing. If a oneshot unit needs to be retried, put the retry in a timer, or make the unit a long-running Type=exec service. Do not try to force it with Restart=always.
RestartSec= and exponential backoff
RestartSec= is the delay before each automatic restart. The manager default is 100 ms, which is fine for a transient failure and terrible for a crash loop: a service that dies immediately will be restarted ten times a second until the start limit trips.
Since systemd 254 there is built-in exponential backoff:
[Service] Restart=on-failure RestartSec=2s RestartSteps=4 RestartMaxDelaySec=32s The intervals become 2s, 4s, 8s, 16s, 32s, 32s, and so on. The ratio is:
(RestartMaxDelaySec / RestartSec) ^ (1 / RestartSteps) and the maximum is reached after RestartSteps + 1 steps. RestartSteps= only works when RestartMaxDelaySec= is also set and RestartSec= is nonzero; with RestartSec=0 the whole backoff feature is inert.
Start limits: the gate in front of every start
These settings live in the [Unit] section since systemd 229 (the old [Service] location is still understood for compatibility):
[Unit] StartLimitIntervalSec=30s StartLimitBurst=3 The defaults come from DefaultStartLimitIntervalSec=10s and DefaultStartLimitBurst=5 in systemd-system.conf(5). In plain English: no more than five starts in any ten-second window, unless the unit overrides it.
Several properties of this limiter cause most of the confusion:
- Every start counts, including
systemctl start, timer activations, socket activations, and automatic restarts. It is not a restart counter; it is a start counter. - The check runs after unit conditions. A start that fails a
Condition...=does not consume an attempt. That is why a unit with a failing condition can look like it is ignoring the limit. - Hitting the limit stops automatic restarts, but the unit is not permanently dead. It can be started later, after the interval, by hand or by a timer or socket. During the interval, even a manual start is refused.
-
systemctl reset-failedflushes the counter. That is the supported way to say "I have fixed the problem, let me try again now." -
StartLimitIntervalSec=0disables rate limiting entirely.infinityinstead caps the total number of starts regardless of how far apart they are. - Garbage collection flushes the counters too. A unit that is unloaded because nothing references it any more loses its start history. Rate limiting is only meaningful for units that stay loaded.
That last point is subtle, and it is also why the limiter is not a security boundary. It is a brake for crash loops, not an audit mechanism.
A reproducible crash loop
You can watch both mechanisms interact without touching any production service. The following uses a user unit, so no root is required. Create the unit:
mkdir -p ~/.config/systemd/user cat > ~/.config/systemd/user/lyra-restart-demo.service <<'EOF' [Unit] Description=Restart limit demo StartLimitIntervalSec=30s StartLimitBurst=3 [Service] Type=exec ExecStart=/bin/false Restart=always RestartSec=200ms [Install] WantedBy=default.target EOF systemctl --user daemon-reload systemctl --user start lyra-restart-demo.service sleep 3 /bin/false exits 1 immediately, so every start fails. On a systemd 257 host the journal ends like this:
Scheduled restart job, restart counter is at 3. Start request repeated too quickly. Failed with result 'exit-code'. Failed to start lyra-restart-demo.service. And the properties show the state:
systemctl --user show lyra-restart-demo.service \ -p NRestarts -p Result -p ActiveState -p SubState \ -p StartLimitIntervalUSec -p StartLimitBurst NRestarts=3 Result=exit-code ActiveState=failed SubState=failed StartLimitIntervalUSec=30s StartLimitBurst=3 Note what Result= says: exit-code, not "rate limit". It records why the service last failed, not why systemd stopped trying. If you only read Result=, you will miss the limiter completely. Depending on the systemd version, you may instead see a result such as start-limit-hit; do not build tooling that depends on either string alone.
Now try a manual start inside the 30-second window:
systemctl --user start lyra-restart-demo.service It fails. The unit is not broken; the counter is still over budget. Flush it and try again:
systemctl --user reset-failed lyra-restart-demo.service systemctl --user show lyra-restart-demo.service -p NRestarts -p Result -p ActiveState NRestarts=0 Result=success ActiveState=inactive A start now works, and the same crash loop runs until it hits the limit again. Clean up when you are done:
systemctl --user stop lyra-restart-demo.service || true rm ~/.config/systemd/user/lyra-restart-demo.service systemctl --user daemon-reload systemctl --user reset-failed lyra-restart-demo.service || true For a system-wide unit, use sudo systemctl and put the file in /etc/systemd/system/. The semantics are identical; only the manager instance changes.
Diagnosing the real thing
When a unit is sitting in failed and you do not know which mechanism fired, collect these four things before changing any configuration:
systemctl status myapp.service systemctl show myapp.service \ -p Restart -p RestartUSec -p NRestarts -p Result \ -p ActiveState -p SubState \ -p StartLimitIntervalUSec -p StartLimitBurst journalctl -u myapp.service -b -g 'Scheduled restart|repeated too quickly|Failed with result' systemctl list-units --state=failed Read them in order:
-
Restart=tells you the policy.RestartUSec=tells you the configured delay. -
NRestarts=tells you how many automatic restart jobs the manager has scheduled. A number at or aboveStartLimitBurst=is a strong hint that the limiter is involved. - The journal line
Start request repeated too quicklyis the limiter speaking. If you see it, the exit cause is no longer the only problem. -
Result=tells you why the unit last failed. It is useful for the application bug, not for the rate limit.
Once the underlying crash is fixed, systemctl reset-failed myapp.service clears the counter so the next start is allowed immediately. Without it, you may have to wait out the interval.
Recipes that hold up
1. A crash loop should back off, not hammer the box
[Unit] StartLimitIntervalSec=60s StartLimitBurst=5 [Service] Restart=on-failure RestartSec=2s RestartSteps=4 RestartMaxDelaySec=32s This gives five attempts in a minute, with growing gaps between them. The service still ends up failed if it is genuinely broken, but it does not spend the minute in a tight fork/exec loop filling the journal.
2. Do not restart on a configuration error
A missing config file or a bad flag will not fix itself. If the application exits with EX_CONFIG (78, from sysexits.h), exclude that status from the restart logic and fail fast:
[Service] ExecStartPre=/usr/local/bin/myapp --check-config Restart=on-failure RestartPreventExitStatus=78 RestartPreventExitStatus= accepts numeric statuses, status names, and signal names. The pre-start check is not strictly required, but it turns a restart storm into one clear error message.
3. Restart after a clean self-exit
Some daemons exit 0 on purpose to request a restart, for example after a self-update. With Restart=on-failure that clean exit would normally be ignored. RestartForceExitStatus= overrides the classification:
[Service] Restart=on-failure RestartForceExitStatus=0 This works, but be careful: a service that exits 0 immediately will now loop until the start limit stops it. Only use it when exit 0 genuinely means "restart me", not "I am done".
4. Do not fail dependents for a temporary blip
If a service depends on something that briefly disappears, a normal restart goes through the failed state, and units with Requires= or BindsTo= can be torn down with it. RestartMode=direct (systemd 254+) restarts without that failed/inactive transition:
[Service] Restart=on-failure RestartMode=direct ExecStopPost= still runs, but OnSuccess= and OnFailure= are skipped and dependent units are not notified of the temporary failure. That is exactly what you want for a flaky network dependency, and exactly what you do not want if the failure should propagate.
5. Leave the rate limit alone unless you have a reason
StartLimitIntervalSec=0 is a real setting and it does what it says: no rate limiting at all. It is also how a broken service turns into a host-wide CPU and disk problem. If a service must retry forever, give it a long interval and a large burst with exponential backoff, not an infinite one.
The short version
-
Restart=matches the cause of death: exit code, signal, timeout, or watchdog. Read the matrix, not the name. - A clean exit includes 0 and, for non-oneshot services, SIGHUP/SIGINT/SIGTERM/SIGPIPE, plus
SuccessExitStatus=. -
Type=oneshotcannot useRestart=alwaysoron-success; use a timer. -
RestartSec=defaults to 100 ms. AddRestartSteps=andRestartMaxDelaySec=for backoff. - Start limits count all starts, not just restarts, and can refuse a manual start inside the window.
-
systemctl reset-failedclears the counter;StartLimitIntervalSec=0disables the brake entirely. -
Result=is not proof that the rate limit fired. The journal message andNRestarts=are.
The next time a unit "stops restarting", do not raise the burst blindly. Find out whether the process is dying for a reason that Restart= classifies as restartable, and whether the limiter has already decided the unit has had enough starts for now.
Sources
- systemd.service(5), "Restart=", "RestartSec=", "RestartSteps=", "RestartMaxDelaySec=", "RestartMode=", "SuccessExitStatus=", "RestartPreventExitStatus=", "RestartForceExitStatus=", Table 1: https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
- systemd.unit(5), "StartLimitIntervalSec=, StartLimitBurst=": https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html
- systemd-system.conf(5), "DefaultStartLimitIntervalSec=, DefaultStartLimitBurst=", "DefaultRestartSec=": https://www.freedesktop.org/software/systemd/man/latest/systemd-system.conf.html
- systemd 229 release notes (start-limit settings moved from [Service] to [Unit], still accepted at the old location): https://github.com/systemd/systemd/blob/main/NEWS
-
sysexits.h,EX_CONFIG= 78: https://man.freebsd.org/cgi/man.cgi?query=sysexits&sektion=3 - Commands and observed output in this article were tested on systemd 257.13 (Debian 13) with a user manager. Values that changed in earlier releases are noted where relevant.

