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 whenUse when
simpleImmediately after fork(), before execve() even runsDefault with ExecStart=. Foreground process, no readiness signal
execAfter fork() and a successful execve()Like simple but systemctl start reports a bad binary or missing User=
forkingThe parent process exitsClassic double-forking daemons. Pair with PIDFile=
oneshotThe main process exitsScripts and migrations. Add RemainAfterExit=yes to show active afterwards
notifyThe process sends READY=1 via sd_notify(3)Real readiness gating. Sets NotifyAccess=main implicitly
dbusThe name in BusName= appears on the busD-Bus services. Default when BusName= is set
idleLike simple, but execution is delayed until all queued jobs are dispatched, capped at 5sConsole output tidiness only, never for ordering
  • simple is the default when ExecStart= is set and neither Type= nor BusName= is.
  • oneshot is the default when neither Type= nor ExecStart= is given.
  • oneshot is the only type allowed more than one ExecStart= line.
  • A Type=simple unit reports success even if the binary does not exist. Use exec if that matters to you.
  • Type=notify-reload (systemd 253+) additionally handles RELOADING=1 for 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 causenoalwayson-successon-failureon-abnormalon-aborton-watchdog
Clean exit code or signal—XX————
Unclean exit code—X—X———
Unclean signal—X—XXX—
Timeout—X—XX——
Watchdog—X—XX—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=5

More 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 unit

Fixes, in order of preference:

  1. Fix the crash. The rate limit is telling you something.
  2. Raise RestartSec= so five attempts take longer than the interval.
  3. Loosen the window, for example StartLimitIntervalSec=60s with StartLimitBurst=10.
  4. StartLimitIntervalSec=0 disables 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

DirectiveDefaultMeaning
TimeoutStartSec=DefaultTimeoutStartSec, usually 90s. Disabled for oneshotHow long start-up may take
TimeoutStopSec=DefaultTimeoutStopSec, usually 90sWait after SIGTERM before SIGKILL
TimeoutAbortSec=falls back to TimeoutStopSecGrace period after a watchdog abort, so a core dump can be written
TimeoutSec=—Shorthand for start plus stop
RuntimeMaxSec=infinityHard cap on total run time, then failure
WatchdogSec=0, offMax 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)
StatusMeaning
203/EXECThe binary could not be executed. Wrong path, not executable, bad interpreter line
200/CHDIRWorkingDirectory= does not exist
216/GROUP, 217/USERGroup= or User= does not exist
226/NAMESPACEA sandboxing directive could not set up its namespace
227/NO_NEW_PRIVILEGESNoNewPrivileges= conflicts with something the unit needs
238/STATE_DIRECTORYStateDirectory= could not be created
1/FAILUREYour program exited non-zero
killed, signal=TERMStop 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.