systemd service Type=: simple, forking, notify
Your unit says active but the daemon is gone. Pick the right systemd Type= for simple, exec, forking, oneshot and notify, and find the real main PID.
Why does systemd report a unit as active when the process died
A systemd service unit stays active while the one process systemd calls the main process is alive, and Type= in the [Service] section decides which process that is. Pick the wrong value and systemd ends up watching a shell wrapper or a short lived parent while the daemon you care about dies inside the same unit. The unit is telling you the truth about the process it was told to watch.
Changing the restart policy will not help here. Restart= acts when the main process exits, so Restart=always never fires while the main PID (process identifier) belongs to something that is still running. Fix Type= first. What systemd does after the main process really exits is a separate decision, covered in the guide to Restart= and RestartSec=.
What does Type= actually decide
Every Type= value answers two questions at once. When may systemd consider this unit started, and which process is the main one.
The first answer controls ordering. A unit that names yours in After= waits until systemd calls yours started. A Type= that reports "started" too early lets dependent units run before your service can answer them.
The second answer controls supervision. systemd places every process a unit spawns into a cgroup (control group), a kernel feature that groups processes so they can be limited and killed together. The cgroup is how systemctl stop cleans up: KillMode= defaults to control-group, so stopping a unit signals every process inside it. The main PID is narrower. It is the single process whose exit ends the unit, and whose exit status becomes the unit's result. Reading the cgroup as if it were the main PID is where the confusion starts.
Type=simple reports started before the binary runs
Type=simple is the default when ExecStart= is set and neither Type= nor BusName= is present. systemd creates the process, considers the unit started right away, and treats that process as the main PID. Follow-up units begin immediately, before the service binary has even been executed.
That last detail explains a common surprise. A typo in the ExecStart= path still produces a start job that succeeds, and the failure arrives a moment later when the execution fails. systemd records that case with exit code 203, which its own table names EXEC and defines as a failure to execute the service binary. So systemctl start returning without an error does not prove your binary exists.
Use simple for a program that stays in the foreground and never moves itself into the background. That covers most modern daemons and almost anything you write yourself.
Type=exec waits for the program to actually start
Type=exec is simple with one more step. systemd considers the unit started only after both the fork and the execution of the binary have succeeded. A missing binary or a User= that cannot be resolved now fails the start job itself, instead of reporting success and failing quietly a moment later.
Type=exec arrived in systemd 240, so every current server distribution has it. Ubuntu 24.04 ships systemd 255 and Debian 13 ships systemd 257, as of August 2026. Check yours with systemctl --version.
The cost is one extra synchronisation step at start. The gain is an honest exit status from systemctl start. For a foreground program, prefer exec over simple.
Type=forking, and how the main PID gets lost
Type=forking tells systemd that the process in ExecStart= will fork a child and then exit on purpose. systemd waits for that first process to exit and only then calls the unit started. The child left behind is the daemon.
The difficulty is identity. The process systemd launched is gone, so systemd has to work out which survivor is the main one. Set PIDFile= to the file the daemon writes, normally a path under /run, and systemd reads the PID from it. systemd also checks that the PID in that file refers to a process that already belongs to this service, so a stale file naming an unrelated process is rejected rather than trusted.
Without PIDFile=, GuessMainPID= applies, and it defaults to yes. The guess is only reliable when the service settles into a single process. The manual states the limit plainly: if the daemon consists of more than one process, the guess can be wrong, and failure detection stops working. A unit can also end up with a main PID of 0, which means systemd has nothing at all to supervise.
Most daemons that fork also have a switch that keeps them in the foreground. Use that switch with Type=exec and delete the PIDFile= line. Fewer moving parts means fewer ways to lose the PID.
Type=oneshot for work that finishes
Type=oneshot expects the process to run and exit. systemd calls the unit started only after it has exited, which makes oneshot the right shape for anything another unit must wait for. It is also the implied default when a unit specifies neither Type= nor ExecStart=.
Two behaviours are specific to oneshot. It is the only type that accepts more than one ExecStart= line, and those lines run in order. Its start timeout is also disabled by default, so a oneshot that hangs waits forever unless you set TimeoutStartSec= yourself.
After the process exits, the unit returns to inactive. RemainAfterExit=yes keeps it active with no process running at all. That is the deliberate version of the symptom at the top of this page, and it is correct when the unit's job was to leave state behind rather than to keep something running: loading a firewall ruleset, or bringing up a container stack. It is the pattern behind a Docker Compose stack that comes back after a reboot, where the unit runs the compose command, exits, and stays active because the containers it started outlive it. A oneshot unit is also what a schedule triggers, which is the other half of running a job on a systemd timer instead of cron.
Type=notify lets the service say when it is ready
Type=notify moves the decision to the service. systemd holds the start job open until the process sends READY=1 over a Unix socket whose path it receives in the NOTIFY_SOCKET environment variable. The C interface is sd_notify(3), and many servers support it already.
This is the accurate answer to the question "is it started". simple and exec report started before the service has read its configuration or opened its listening socket, so a dependent unit can start too early and fail its first connection. notify reports started at the moment the service itself says it is ready.
systemd accepts that message from the main process only, which is what NotifyAccess=main means, and Type=notify implies it. If the message comes from a child or a helper, set NotifyAccess=all. A shell script can call systemd-notify --ready, but that runs as a separate short lived process, so it needs NotifyAccess=all and systemd may not be able to attribute a message whose sender has already exited. A service that speaks the protocol itself is more reliable.
Two related settings are worth knowing. Type=notify-reload, available since systemd 253, extends the same handshake to reloads, so systemctl reload returns when the service reports the reload is finished instead of returning when the signal was sent. WatchdogSec= asks a notifying service to send a keep-alive message on an interval, and systemd treats a missed deadline as a failure.
Type=dbus and Type=idle
Type=dbus waits until the service takes a name on D-Bus, the message bus that system and desktop services use to talk to each other. It requires BusName=, and it becomes the default as soon as BusName= is set. Use it only for a service that really registers a bus name.
Type=idle behaves like simple, but it delays running the program until queued jobs have been dispatched, with a five second cap. It exists so that console output at boot does not interleave with status messages. It is not an ordering tool, and it does not belong on a normal service.
Why a wrapper script strands systemd on the wrong PID
Here is the shape that produces the original symptom.
[Service]
Type=simple
ExecStart=/opt/app/run.sh#!/bin/bash
export APP_ENV=production
/opt/app/bin/server --config /etc/app.yaml &
/opt/app/bin/exporter --port 9101systemd records the shell as the main PID. The shell stays alive while exporter runs in the foreground. If server dies, the shell does not notice, so the main PID is still alive, the unit is still active, and Restart= has nothing to act on. Both processes sit in the unit's cgroup the whole time, so systemctl stop still cleans up correctly. Supervision is what broke, not cleanup.
The fix depends on how many long running processes the unit really has.
If there is one, replace the shell with it.
#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yamlexec replaces the shell with the named program and keeps the same PID, so the PID systemd recorded now belongs to the daemon. Better again, remove the wrapper. Environment= and EnvironmentFile= carry the variables, and ExecStartPre= carries the setup step, so systemd can launch the daemon directly and know its PID by construction.
If there are two, no single PID represents the unit. Split them into two units and order them with After= and Wants=. One unit per process is the arrangement systemd supervises well, and it is the only way each process gets its own restart behaviour.
What ExitType=cgroup changes
ExitType= was added in systemd 250. The default is main: the unit is considered stopped when the main process exits. With ExitType=cgroup, the unit is considered running for as long as any process in its cgroup is alive.
[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcherThis solves one specific problem. A launcher that starts the real work and then exits would, under ExitType=main, make systemd consider the unit stopped and kill the survivors. With ExitType=cgroup the unit follows the whole group instead.
Be clear about what it does not solve. ExitType=cgroup keeps a unit active while at least one process lives, so a unit holding two daemons stays active after one of them dies. It fixes the launcher case. It does not turn one unit into a supervisor of several independent processes. ExitType= also cannot be combined with Type=oneshot.
The cgroup is where resource accounting lands too, so limits such as MemoryMax= and CPUQuota= apply to every process the unit spawned, whatever Type= says about the main PID. That side of it is in capping a service's memory and CPU with systemd.
How to find the process systemd is actually watching
Work through this on the unit you are debugging, in order. Read what systemd loaded, then read what it tracks, then compare that with the process table.
systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.servicesystemctl cat prints the unit file together with every drop-in that applies to it, so you are reading what systemd loaded rather than the file you remember editing. systemctl show prints the effective values, including the defaults you never wrote down. Note the value of MainPID before moving on.
systemd-cgls --unit=app.service
ps -o pid,ppid,stat,etime,args -p "$(systemctl show -p MainPID --value app.service)"systemd-cgls lists every process in the unit's cgroup. The ps line describes the single process systemd supervises. Read the two together. A MainPID of 0 means systemd has no process to watch. A MainPID that resolves to a shell while the cgroup also holds your daemon is the wrapper case above. A cgroup with more processes than you expected means a launcher or a forking daemon is involved.
systemctl status app.service
journalctl -u app.service -bsystemctl status prints the state line and the cgroup tree together, so it often answers both questions at once. journalctl -u limited to this boot with -b shows the start and stop events systemd recorded for the unit, with the exit codes it saw. If the daemon writes to its own log file instead of the journal, read that file as well, because systemd can only record what reached it.
When you change Type=, reload and restart.
systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.servicesystemd-analyze verify parses the file and reports settings it cannot accept. daemon-reload makes systemd re-read unit files from disk. A changed Type= does not apply to an already running unit, so the restart is required, not optional.
Then test the change. Take the PID of the process you actually care about from systemd-cgls and kill it. Run systemctl is-active app.service straight after. If Type= is right, the unit leaves the active state. If it stays active, systemd is still watching something else.
Which systemd service Type= should you use
- A program that stays in the foreground:
Type=exec. - A program that supports readiness notification:
Type=notify, andnotify-reloadif it confirms reloads too. - A daemon that insists on moving into the background:
Type=forkingwithPIDFile=, or its foreground switch withType=exec. - A script that does work and exits:
Type=oneshot, plusRemainAfterExit=yeswhen the point was to leave state behind. - A launcher that exits while its children keep running:
Type=simplewithExitType=cgroup.
If you are unsure which one a third party daemon needs, read its packaged unit file first. Running systemctl cat on a unit the distribution shipped shows the Type= upstream chose, and that choice has been tested by more people than yours.
FAQ
Why does my systemd unit stay active when the process died?
Because the process systemd treats as the main one is still alive. systemd watches a single PID per service, chosen according to Type=, rather than every process in the unit's cgroup. A wrapper script started with Type=simple is the usual cause: the shell is the main PID, so the unit stays active when a daemon that the shell launched in the background exits. Run systemctl show -p MainPID app.service, then list the unit's cgroup with systemd-cgls --unit=app.service, and compare the two.
What is the difference between Type=simple and Type=exec?
Type=simple considers the unit started as soon as systemd has created the process, before the binary has been executed, so a wrong path in ExecStart= still gives a successful start job followed by a failure. Type=exec waits until the execution has succeeded, so that failure is reported by the start job itself. Both treat the same process as the main PID. Type=exec needs systemd 240 or newer.
Do I still need PIDFile= with Type=forking?
Yes, whenever the daemon writes one. Without it, systemd falls back to GuessMainPID=, which is a guess and is only reliable for a service that settles into a single process. When the guess is wrong or impossible, failure detection and automatic restarting stop working for that unit. Point PIDFile= at the exact path the daemon writes, normally under /run.
When should I use RemainAfterExit=yes?
When the unit's purpose was to change system state rather than to keep a process running. A Type=oneshot unit that loads firewall rules or starts a container stack exits as soon as its work is done, and without RemainAfterExit=yes the unit goes inactive, which leaves systemctl stop with nothing to stop and no way to run an ExecStop= cleanup. With it, the unit stays active with no processes, and that is intended here.
Does changing Type= require a daemon-reload?
Yes, and a restart of the unit as well. systemctl daemon-reload makes systemd re-read the unit files on disk, but a running instance keeps the Type= it started with. Run sudo systemctl daemon-reload and then sudo systemctl restart app.service before testing, otherwise you are still watching the old supervision behaviour.