systemd Type=: simple, forking, notify Which One?
Unit dey show active but daemon don die? Learn when to use simple, exec, forking, oneshot or notify, and how systemd picks the real main PID.
Why systemd dey report unit as active when process don die
A systemd service unit go remain active as long as the one process wey systemd call the main process still dey alive, and Type= for the [Service] section dey decide which process be that one. If you choose wrong value, systemd fit end up dey watch shell wrapper or short-lived parent, while the daemon wey you care about don die inside the same unit. The unit dey tell you the truth about the process wey you instruct am to watch.
Changing the restart policy no go help for here. Restart= dey act when the main process exits, so Restart=always no go ever trigger while the main PID (process identifier) still belong to something wey dey run. Fix Type= first. Wetin systemd go do after the main process really exits na separate decision, and the guide to Restart= and RestartSec= cover am.
Wetin Type= actually dey decide
Every Type= value dey answer two questions at once. When systemd fit consider this unit started, and which process be the main one.
The first answer controls ordering. A unit wey name your own for After= go wait until systemd call your own started. A Type= wey report say "started" too early fit make dependent units run before your service fit answer dem.
The second answer controls supervision. systemd dey put every process wey unit spawn inside a cgroup (control group), na kernel feature wey dey group processes so dem fit limit and kill dem together. Na the cgroup systemctl stop dey use clean up: KillMode= default na control-group, so when unit stop, e dey signal every process wey dey inside am. The main PID narrow pass that one. Na the single process wey e exit go end the unit, and na im exit status become the unit result. Confusion dey start when person read the cgroup as if na the main PID.
Type=simple report say e start before binary run
Type=simple na the default when ExecStart= dey set and Type= plus BusName= no dey present. systemd go create the process, count the unit as started immediately, and treat that process as the main PID. Follow-up units go start immediately, before service binary even execute.
That last detail explain one common surprise. Typo for ExecStart= path still fit produce start job wey succeed, then failure go come small time later when execution fail. systemd dey record this case with exit code 203. The table wey systemd provide call am EXEC and define am as failure to execute service binary. So systemctl start returning without error no prove say your binary dey exist.
Use simple for program wey stay for foreground and never move itself go background. This cover most modern daemons and almost anything wey you write yourself.
Type=exec dey wait make program actually start
Type=exec na simple with one extra step. systemd go count the unit as started only after both the fork and execution of the binary don succeed. If binary no dey, or User= no fit resolve, the start job go fail directly instead of reporting success and failing quietly shortly after.
Type=exec enter systemd 240, so every current server distribution get am. Ubuntu 24.04 dey ship systemd 255, and Debian 13 dey ship systemd 257, as of August 2026. Check your own with systemctl --version.
The cost na one extra synchronisation step when e dey start. The benefit na honest exit status from systemctl start. For foreground program, prefer exec instead of simple.
Type=forking, and how the main PID fit lost
Type=forking dey tell systemd say the process for ExecStart= go fork one child, then e go exit on purpose. systemd go wait make that first process exit before e call the unit started. The child wey remain na the daemon.
The problem na identity. The process wey systemd launch don disappear, so systemd gats work out which remaining process be the main one. Set PIDFile= to the file wey the daemon write, normally a path under /run, and systemd go read the PID from there. systemd still check say the PID for that file belong to a process wey already dey part of this service. So, e go reject stale file wey name unrelated process instead of trusting am.
If PIDFile= no dey, GuessMainPID= go apply, and e dey default to yes. This guess only reliable when the service settle as one process. The manual talk the limit clear: if the daemon get more than one process, the guess fit wrong, and failure detection go stop to work. Unit fit also end up with main PID of 0. This mean say systemd get nothing at all to supervise.
Most daemons wey fork get switch wey keep dem for foreground. Use that switch with Type=exec and delete PIDFile= line. Fewer moving parts mean fewer ways to lose the PID.
Type=oneshot for work wey go finish
Type=oneshot dey expect make process run finish then exit. systemd go call unit “started” only after e don exit, so oneshot na the correct pattern for anything wey another unit must wait for. Na also the implied default when unit no specify either Type= or ExecStart=.
Two behaviours dey specific to oneshot. Na the only type wey accept more than one ExecStart= line, and dem go run in order. E start timeout dey disabled by default too, so oneshot wey hang go wait forever unless you set TimeoutStartSec= by yourself.
After process don exit, unit go return to inactive. RemainAfterExit=yes go keep am active without any process running at all. Na the deliberate version of the symptom for top of this page, and e correct when the unit job na to leave state behind instead of keeping something running: loading firewall ruleset, or bringing up container stack. Na the pattern behind Docker Compose stack wey dey come back after reboot, where unit run the compose command, exit, and remain active because the containers wey e start outlive am. A oneshot unit na also wetin schedule dey trigger, and na the other half of running a job with systemd timer instead of cron.
Type=notify mean say the service fit talk when e don ready
Type=notify shifts the decision go the service. systemd go keep the start job open until the process send READY=1 through a Unix socket wey e receive the path for inside the NOTIFY_SOCKET environment variable. The C interface na sd_notify(3), and many servers already support am.
This na the correct answer to the question “e don start?” simple and exec dey report say e don start before the service read its configuration or open its listening socket. Because of this, dependent unit fit start too early and fail the first connection. notify dey report say e don start exactly when the service itself talk say e ready.
systemd dey accept that message only from the main process. Na this NotifyAccess=main mean, and Type=notify imply am. If na child or helper send the message, set NotifyAccess=all. Shell script fit call systemd-notify --ready, but e go run as separate short-lived process. So e need NotifyAccess=all, and systemd fit no know say the message come from am if the sender don already exit. Service wey speak the protocol by itself dey more reliable.
Two related settings dey important to know. Type=notify-reload, wey dey available since systemd 253, extend the same handshake to reloads. This mean systemctl reload go return when the service report say reload don finish, instead of returning immediately after the signal send. WatchdogSec= tell notifying service make e send keep-alive message at regular interval, and systemd dey treat missed deadline as failure.
Type=dbus and Type=idle
Type=dbus dey wait until service take a name for D-Bus, the message bus wey system and desktop services dey use to communicate with each other. E require BusName=, and e become the default once BusName= set. Use am only for service wey really register a bus name.
Type=idle behave like simple, but e delay running the program until queued jobs don dispatch, with five second limit. E dey prevent console output for boot from mixing with status messages. E no be ordering tool, and e no belong for normal service.
Why wrapper script dey leave systemd for wrong PID
Na this pattern dey cause the original problem.
[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 dey record the shell as the main PID. The shell remain alive while exporter dey run for foreground. If server die, the shell no notice am. So the main PID still dey alive, the unit still dey active, and Restart= no get anything to act on. Both processes dey inside the unit cgroup all the time, so systemctl stop still dey clean dem up correctly. Na supervision break, no be cleanup.
The fix depend on how many long-running processes the unit really get.
If na one, replace the shell with am.
#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yamlexec replace the shell with the named program and keep the same PID. So the PID wey systemd record now belong to the daemon. Better still, remove the wrapper. Environment= and EnvironmentFile= carry the variables, while ExecStartPre= carry the setup step. This make systemd launch the daemon directly and know its PID from the start.
If na two, no single PID fit represent the unit. Split dem into two units and arrange their order with After= and Wants=. One unit for each process na the arrangement systemd dey supervise well. Na only this way each process fit get its own restart behaviour.
Wetin ExitType=cgroup dey change
ExitType= bin add for systemd 250. Default na main: system dey consider unit stopped once main process exit. With ExitType=cgroup, system dey consider unit dey run as long as any process dey alive inside its cgroup.
[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcherThis one solve one particular problem. Launcher wey start the real work and then exit go, under ExitType=main, make systemd consider unit stopped and kill the processes wey remain. With ExitType=cgroup, unit dey follow the whole group instead.
Make you understand wetin e no solve. ExitType=cgroup keep unit active as long as at least one process dey alive, so unit wey hold two daemons go remain active after one of dem die. E fix the launcher case. E no turn one unit into supervisor for several independent processes. ExitType= too no fit work together with Type=oneshot.
Na for cgroup resource accounting dey happen too, so limits like MemoryMax= and CPUQuota= apply to every process wey the unit start, no matter wetin Type= talk about the main PID. That part dey for how to limit service memory and CPU with systemd.
How to find the process wey systemd dey actually watch
Follow these steps for the unit wey you dey debug, in order. First read wetin systemd load, then read wetin e dey track, then compare am with the process table.
systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.servicesystemctl cat dey print the unit file together with every drop-in wey apply to am, so you dey read wetin systemd load instead of the file wey you remember say you edit. systemctl show dey print the effective values, including the defaults wey you never write down. Note the value of MainPID before you continue.
systemd-cgls --unit=app.service
ps -o pid,ppid,stat,etime,args -p "$(systemctl show -p MainPID --value app.service)"systemd-cgls dey list every process for the unit's cgroup. The ps line dey describe the single process wey systemd dey supervise. Read both together. MainPID of 0 mean say systemd no get process to watch. MainPID wey resolve to a shell while the cgroup still hold your daemon na the wrapper case wey we mention before. If cgroup get more processes than you expect, launcher or forking daemon dey involved.
systemctl status app.service
journalctl -u app.service -bsystemctl status dey print the state line and the cgroup tree together, so e often answer both questions at once. journalctl -u limited to this boot with -b dey show the start and stop events wey systemd record for the unit, together with the exit codes wey e see. If the daemon dey write to im own log file instead of the journal, read that file too, because systemd fit only record wetin reach am.
When you change Type=, reload and restart am.
systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.servicesystemd-analyze verify dey parse the file and report settings wey e no fit accept. daemon-reload make systemd read the unit files from disk again. Changed Type= no go apply to unit wey already dey run, so restart dey required; e no be optional.
Then test the change. Take the PID of the process wey you actually care about from systemd-cgls and kill am. Run systemctl is-active app.service immediately after. If Type= correct, the unit go comot from active state. If e remain active, systemd still dey watch another thing.
Which systemd service Type= you suppose use
- Program wey dey stay for foreground:
Type=exec. - Program wey support readiness notification:
Type=notify, andnotify-reloadif e dey confirm reloads too. - Daemon wey insist say e go move enter background:
Type=forkingwithPIDFile=, or e foreground switch withType=exec. - Script wey do work and exit:
Type=oneshot, plusRemainAfterExit=yeswhen the main point na to leave state behind. - Launcher wey dey exit while e children continue to run:
Type=simplewithExitType=cgroup.
If you no sure which one third-party daemon need, first read the unit file wey dem package. If you run systemctl cat on unit wey the distribution ship, e go show the Type= wey upstream choose. More people don test that choice pass your own.
FAQ
Why my systemd unit dey stay active when the process don die?
Na because the process wey systemd dey treat as the main one still dey alive. systemd dey watch one PID for each service, based on Type=, instead of every process for the unit's cgroup. Wrapper script wey start with Type=simple na the usual cause: the shell na the main PID, so the unit dey stay active when daemon wey the shell start for background comot. Run systemctl show -p MainPID app.service, then list the unit's cgroup with systemd-cgls --unit=app.service, and compare both.
Wetin be the difference between Type=simple and Type=exec?
Type=simple dey count the unit as started as soon as systemd don create the process, before e execute the binary. So, wrong path for ExecStart= still fit give successful start job, then fail afterward. Type=exec dey wait until the execution succeed, so the start job itself report the failure. Both dey treat the same process as the main PID. Type=exec need systemd 240 or newer.
I still need PIDFile= with Type=forking?
Yes, anytime the daemon dey write one. Without am, systemd fall back to GuessMainPID=. Na guess be that, and e only reliable for service wey settle into one process. If the guess wrong or impossible, failure detection and automatic restart no go work again for that unit. Point PIDFile= to the exact path wey the daemon dey write, normally under /run.
When I suppose use RemainAfterExit=yes?
Use am when the unit's purpose na to change system state, instead of keeping process dey run. A Type=oneshot unit wey load firewall rules or start container stack go exit as soon as e finish the work. Without RemainAfterExit=yes, the unit go become inactive, so systemctl stop get nothing to stop and no way to run an ExecStop= cleanup. With am, the unit go stay active without processes, and na so e suppose be for this case.
Changing Type= need daemon-reload?
Yes, and you also need restart the unit. systemctl daemon-reload make systemd read the unit files from disk again, but running instance still dey use the Type= wey e start with. Run sudo systemctl daemon-reload and then sudo systemctl restart app.service before you test. Otherwise, you still dey watch the old supervision behaviour.