systemd Cheatsheet
Service Types and Restart
Use this systemd reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Type=, the Whole Table
Type= answers exactly one question: when does systemd consider the unit started and move on to the next one? Everything ordered After= your unit waits for that moment.
Type= | Considered started when | Use when |
|---|---|---|
simple | Immediately after fork(), before execve() even runs | Default with ExecStart=. Foreground process, no readiness signal |
exec | After fork() and a successful execve() | Like simple but systemctl start reports a bad binary or missing User= |
forking | The parent process exits | Classic double-forking daemons. Pair with PIDFile= |
oneshot | The main process exits | Scripts and migrations. Add RemainAfterExit=yes to show active afterwards |
notify | The process sends READY=1 via sd_notify(3) | Real readiness gating. Sets NotifyAccess=main implicitly |
dbus | The name in BusName= appears on the bus | D-Bus services. Default when BusName= is set |
idle | Like simple, but execution is delayed until all queued jobs are dispatched, capped at 5s | Console output tidiness only, never for ordering |
simpleis the default whenExecStart=is set and neitherType=norBusName=is.oneshotis the default when neitherType=norExecStart=is given.oneshotis the only type allowed more than oneExecStart=line.- A
Type=simpleunit reports success even if the binary does not exist. Useexecif that matters to you. Type=notify-reload(systemd 253+) additionally handlesRELOADING=1for graceful reloads.
[Service] Type=oneshot RemainAfterExit=yes ExecStart=/usr/local/bin/step-one ExecStart=/usr/local/bin/step-two
Restart=, the Whole Table
Rows are what happened, columns are the Restart= value. An X means systemd restarts the unit.
| 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 |
no is the default. on-failure is the right answer for a long-running service. A clean exit means exit code 0, an exit status listed in SuccessExitStatus=, or (for types other than oneshot) SIGHUP, SIGINT, SIGTERM or SIGPIPE.
Nothing restarts when you stopped it. systemctl stop and any systemd-initiated stop are excluded, as are exit codes listed in RestartPreventExitStatus=. RestartForceExitStatus= forces the opposite.
[Service] Restart=on-failure RestartSec=5s # default is 100ms, far too fast SuccessExitStatus=143 SIGTERM RestartPreventExitStatus=78 # EX_CONFIG, do not loop on a bad config
"start request repeated too quickly"
This is the error everyone hits, and it is not a bug. Rate limiting lives in [Unit]:
[Unit]
StartLimitIntervalSec=10s
StartLimitBurst=5More than StartLimitBurst starts inside StartLimitIntervalSec and systemd refuses to start the unit at all, logging:
myapp.service: Start request repeated too quickly.
myapp.service: Failed with result 'start-limit-hit'.The unit is now stuck in failed and systemctl start will not help. Clear the counter first:
systemctl reset-failed myapp.service
systemctl start myapp.service
systemctl reset-failed # clear every failed unitFixes, in order of preference:
- Fix the crash. The rate limit is telling you something.
- Raise
RestartSec=so five attempts take longer than the interval. - Loosen the window, for example
StartLimitIntervalSec=60swithStartLimitBurst=10. StartLimitIntervalSec=0disables rate limiting entirely. Use it only for something that genuinely must never stay down.
StartLimitAction= (v229+) takes an extra step when the limit is hit, one of none (default), reboot, reboot-force, reboot-immediate, poweroff, poweroff-force, poweroff-immediate or exit. Restart= counts toward the same limit, so a crash loop plus StartLimitAction=reboot will reboot the machine.
Timeouts and the Watchdog
| Directive | Default | Meaning |
|---|---|---|
TimeoutStartSec= | DefaultTimeoutStartSec, usually 90s. Disabled for oneshot | How long start-up may take |
TimeoutStopSec= | DefaultTimeoutStopSec, usually 90s | Wait after SIGTERM before SIGKILL |
TimeoutAbortSec= | falls back to TimeoutStopSec | Grace period after a watchdog abort, so a core dump can be written |
TimeoutSec= | — | Shorthand for start plus stop |
RuntimeMaxSec= | infinity | Hard cap on total run time, then failure |
WatchdogSec= | 0, off | Max gap between WATCHDOG=1 pings before SIGABRT |
Pass infinity to disable any of the timeouts. TimeoutStartFailureMode= and TimeoutStopFailureMode= take terminate (default), abort or kill.
Watchdog only works if your process actually pings. Pair WatchdogSec=30s with Restart=on-watchdog (or on-failure) and call sd_notify(0, "WATCHDOG=1") on a timer well under 30 seconds. $WATCHDOG_USEC is set in the process environment so the code can read the deadline instead of hardcoding it.
Reading the Exit Result
systemctl status prints the outcome twice, and both halves matter.
Active: failed (Result: exit-code) since Mon 2026-09-14 11:02:31 UTC Process: 4411 ExecStart=/usr/local/bin/myapp (code=exited, status=203/EXEC)
| Status | Meaning |
|---|---|
203/EXEC | The binary could not be executed. Wrong path, not executable, bad interpreter line |
200/CHDIR | WorkingDirectory= does not exist |
216/GROUP, 217/USER | Group= or User= does not exist |
226/NAMESPACE | A sandboxing directive could not set up its namespace |
227/NO_NEW_PRIVILEGES | NoNewPrivileges= conflicts with something the unit needs |
238/STATE_DIRECTORY | StateDirectory= could not be created |
1/FAILURE | Your program exited non-zero |
killed, signal=TERM | Stop timeout expired or something signalled it |
Result: values you will see are exit-code, signal, timeout, core-dump, watchdog, start-limit-hit, oom-kill, protocol and resources. Anything in the 200s is systemd failing to launch the process, so the fault is in the unit file, not in your code.